Merge branch 'Indore-10.0.x' into Indore-2017.1.x
authorDmitry Neverov <dmitry.neverov@gmail.com>
Tue, 24 Oct 2017 11:41:58 +0000 (13:41 +0200)
committerDmitry Neverov <dmitry.neverov@gmail.com>
Tue, 24 Oct 2017 11:41:58 +0000 (13:41 +0200)
136 files changed:
.idea/.name
.idea/artifacts/plugin.xml
.idea/artifacts/vcs_worker.xml
.idea/libraries/commons_codec.xml [new file with mode: 0644]
.idea/libraries/commons_codec_1_3.xml [deleted file]
.idea/libraries/httpclient.xml [moved from .idea/libraries/httpclient_4_3_4.xml with 51% similarity]
git-agent/git-agent.iml
git-agent/src/META-INF/build-agent-plugin-git.xml
git-agent/src/META-INF/credentials-helper.bat
git-agent/src/META-INF/credentials-helper.sh
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/AgentPluginConfig.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/BuildContext.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/Context.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/GCIdleTask.java [new file with mode: 0644]
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/GitAgentVcsSupport.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/GitBuildProgressLogger.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/GitCommandLine.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/GitProgressLogger.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/NativeGitFacade.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/NoBuildContext.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/PluginConfigImpl.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/RemoteRepositoryConfigurator.java [new file with mode: 0644]
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/UpdaterImpl.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/UpdaterWithAlternates.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/UpdaterWithMirror.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/command/FetchCommand.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/command/ScriptGen.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/command/impl/CommandUtil.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/command/impl/FetchCommandImpl.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/command/impl/GitProgressListener.java
git-common/git-common.iml
git-common/src/jetbrains/buildServer/buildTriggers/vcs/git/GitVcsRoot.java
git-common/src/jetbrains/buildServer/buildTriggers/vcs/git/MirrorManager.java
git-common/src/jetbrains/buildServer/buildTriggers/vcs/git/MirrorManagerImpl.java
git-dsl/Git.xml
git-server-tc/git-server-tc.iml
git-server-tc/src/META-INF/build-server-plugin-git-tc.xml
git-server-tc/src/jetbrains/buildServer/buildTriggers/vcs/git/health/GitGcErrorsHealthPage.java [new file with mode: 0644]
git-server-tc/src/jetbrains/buildServer/buildTriggers/vcs/git/health/GitGcErrorsHealthReport.java [new file with mode: 0644]
git-server-tc/src/jetbrains/buildServer/buildTriggers/vcs/git/health/GitNotFoundHealthPage.java [new file with mode: 0644]
git-server-tc/src/jetbrains/buildServer/buildTriggers/vcs/git/health/GitNotFoundHealthReport.java [new file with mode: 0644]
git-server/git-server.iml
git-server/resources/buildServerResources/health/gitGcErrorsReport.jsp [new file with mode: 0644]
git-server/resources/buildServerResources/health/gitNotFoundReport.jsp [new file with mode: 0644]
git-server/src/META-INF/build-server-plugin-git.xml
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/Cleanup.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/CleanupRunner.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/CommitLoaderImpl.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/GcErrors.java [new file with mode: 0644]
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/GitCollectChangesPolicy.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/GitCommitSupport.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/GitFetchService.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/GitLabelingSupport.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/GitMapFullPath.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/GitMergeSupport.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/GitResetCacheHandler.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/GitServerUtil.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/GitUrlSupport.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/GitVcsSupport.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/ModificationDataRevWalk.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/OperationContext.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/PluginConfigImpl.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/RepositoryManager.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/RepositoryManagerImpl.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/RepositoryRevisionCache.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/RevisionsCache.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/ServerPluginConfig.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/VcsAction.java [new file with mode: 0644]
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/VcsOperation.java [new file with mode: 0644]
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/VcsPropertiesProcessor.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/commitInfo/GitCommitsInfoBuilder.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/patch/BulkPatchBuilderImpl.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/patch/GitPatchBuilder.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/submodules/SubmoduleAwareTreeIterator.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/submodules/SubmoduleAwareTreeIteratorFactory.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/submodules/SubmoduleResolverImpl.java
git-tests/data/excluded_broken_submodule/after/dir/b.txt [new file with mode: 0755]
git-tests/data/excluded_broken_submodule/after/dir/d.txt [new file with mode: 0755]
git-tests/data/excluded_broken_submodule/after/dir/q.txt [new file with mode: 0755]
git-tests/data/submodules_and_checkout_rules/after/first-level-submodule/.gitmodules [new file with mode: 0644]
git-tests/data/submodules_and_checkout_rules/after/first-level-submodule/sub-sub/file.txt [new file with mode: 0644]
git-tests/data/submodules_and_checkout_rules/after/first-level-submodule/sub-sub/new file.txt [new file with mode: 0644]
git-tests/data/submodules_and_checkout_rules/after/first-level-submodule/submoduleFile.txt [new file with mode: 0644]
git-tests/data/submodules_and_checkout_rules2/after/first-level-submodule/sub-sub/file.txt [new file with mode: 0644]
git-tests/data/submodules_and_checkout_rules2/after/first-level-submodule/sub-sub/new file.txt [new file with mode: 0644]
git-tests/data/submodules_and_checkout_rules3/after/.gitmodules [new file with mode: 0644]
git-tests/data/submodules_and_checkout_rules3/after/dir/b.txt [new file with mode: 0755]
git-tests/data/submodules_and_checkout_rules3/after/dir/d.txt [new file with mode: 0755]
git-tests/data/submodules_and_checkout_rules3/after/dir/q.txt [new file with mode: 0755]
git-tests/data/submodules_and_checkout_rules3/after/first-level-submodule/.gitmodules [new file with mode: 0644]
git-tests/data/submodules_and_checkout_rules3/after/first-level-submodule/sub-sub/new file.txt [new file with mode: 0644]
git-tests/data/submodules_and_checkout_rules3/after/first-level-submodule/submoduleFile.txt [new file with mode: 0644]
git-tests/data/submodules_and_checkout_rules3/after/readme.txt [new file with mode: 0755]
git-tests/data/submodules_and_checkout_rules4/after/first-level-submodule/sub-sub/file.txt [new file with mode: 0644]
git-tests/git-tests.iml
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/AgentVcsSupportTest.java
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/AutoCheckoutTest.java
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/CleanerTest.java
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/CollectChangesTest.java
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/GitPatchTest.java
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/GitResetCacheHandlerTest.java
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/GitServerUtilTest.java
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/GitSupportBuilder.java
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/GitUrlSupportTest.java
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/GitVcsRootTest.java
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/HttpUrlWithUsernameTest.java [new file with mode: 0644]
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/MapFullPathTest.java
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/PluginConfigBuilder.java
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/RemoteRepositoryConfiguratorTest.java [new file with mode: 0644]
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/RevisionsCacheTest.java
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/SubmoduleTest.java
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/VcsPropertiesProcessorTest.java
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/VcsRootBuilder.java
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/builders/AgentRunningBuildBuilder.java
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/builders/BuildAgentConfigurationBuilder.java
git-tests/src/native-git-testng.xml
lib/common/commons-codec-1.6.jar [deleted file]
lib/common/commons-codec-1.9.jar [new file with mode: 0644]
lib/common/commons-logging-1.1.3.jar [deleted file]
lib/common/commons-logging-1.2.jar [new file with mode: 0644]
lib/common/fluent-hc-4.3.4.jar [deleted file]
lib/common/fluent-hc-4.5.3.jar [new file with mode: 0644]
lib/common/httpclient-4.3.4.jar [deleted file]
lib/common/httpclient-4.5.3.jar [new file with mode: 0644]
lib/common/httpclient-cache-4.3.4.jar [deleted file]
lib/common/httpclient-cache-4.5.3.jar [new file with mode: 0644]
lib/common/httpcore-4.3.2.jar [deleted file]
lib/common/httpcore-4.4.6.jar [new file with mode: 0644]
lib/common/httpmime-4.3.4.jar [deleted file]
lib/common/httpmime-4.5.3.jar [new file with mode: 0644]
lib/common/org.eclipse.jgit-3.7.0.201502260915-r.jar
lib/common/src/commons-codec-1.6-src.zip [deleted file]
lib/common/src/commons-codec-1.9-src.zip [new file with mode: 0644]
lib/common/src/httpcomponents-client-4.3.4-src.zip [deleted file]
lib/common/src/httpcomponents-client-4.5.3-src.zip [new file with mode: 0644]
lib/common/src/org.eclipse.jgit-3.7.0.201502260915-r-sources.jar

index 5487124bf49f4696cf708572e50fe009e6b7a456..2fdf873b57bebb91098ee76c4433c9c8482e6eff 100644 (file)
@@ -1 +1 @@
-git-plugin 10.0.x
\ No newline at end of file
+git-plugin 2017.1.x
\ No newline at end of file
index 8660737ccd081e8368f65583f3b7cba7a3b9b155..636a4d9935bc80e0b9d3a8acff7e90c13563003e 100644 (file)
@@ -16,7 +16,7 @@
         <element id="artifact" artifact-name="git-common.jar" />
         <element id="library" level="project" name="quartz-1.6.0" />
         <element id="library" level="project" name="org.eclipse.egit.github.core-2.4.0-SNAPSHOT" />
-        <element id="library" level="project" name="httpclient-4.3.4" />
+        <element id="library" level="project" name="httpclient" />
         <element id="library" level="project" name="JavaEWAH-0.7.9" />
       </element>
       <element id="directory" name="agent">
@@ -30,7 +30,7 @@
               <element id="directory" name="ssh">
                 <element id="library" level="project" name="Trilead SSH" />
               </element>
-              <element id="library" level="project" name="httpclient-4.3.4" />
+              <element id="library" level="project" name="httpclient" />
             </element>
           </element>
         </element>
index 85b67eac09150d180e117d1418745bc7be4af1d5..71138e6b2598a738b79e0dd9fd04485598b1c64c 100644 (file)
@@ -16,7 +16,7 @@
         <element id="artifact" artifact-name="git-common.jar" />
         <element id="artifact" artifact-name="git-server.jar" />
         <element id="library" level="project" name="org.eclipse.egit.github.core-2.4.0-SNAPSHOT" />
-        <element id="library" level="project" name="httpclient-4.3.4" />
+        <element id="library" level="project" name="httpclient" />
         <element id="library" level="project" name="JavaEWAH-0.7.9" />
       </element>
     </root>
diff --git a/.idea/libraries/commons_codec.xml b/.idea/libraries/commons_codec.xml
new file mode 100644 (file)
index 0000000..3e4bb39
--- /dev/null
@@ -0,0 +1,9 @@
+<component name="libraryTable">
+  <library name="commons-codec">
+    <CLASSES>
+      <root url="jar://$PROJECT_DIR$/lib/common/commons-codec-1.9.jar!/" />
+    </CLASSES>
+    <JAVADOC />
+    <SOURCES />
+  </library>
+</component>
\ No newline at end of file
diff --git a/.idea/libraries/commons_codec_1_3.xml b/.idea/libraries/commons_codec_1_3.xml
deleted file mode 100644 (file)
index 1a006a1..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-<component name="libraryTable">
-  <library name="commons-codec-1.3">
-    <CLASSES>
-      <root url="jar://$TeamCityDistribution$/webapps/ROOT/WEB-INF/lib/commons-codec-1.3.jar!/" />
-    </CLASSES>
-    <JAVADOC />
-    <SOURCES />
-  </library>
-</component>
\ No newline at end of file
similarity index 51%
rename from .idea/libraries/httpclient_4_3_4.xml
rename to .idea/libraries/httpclient.xml
index 3c7f77193fac204d26be3655c0ff6906ba725174..3b46a56595ad2a91f656cb77083489efceae4eb7 100644 (file)
@@ -1,35 +1,37 @@
 <component name="libraryTable">
-  <library name="httpclient-4.3.4">
+  <library name="httpclient">
     <CLASSES>
-      <root url="jar://$PROJECT_DIR$/lib/common/httpcore-4.3.2.jar!/" />
-      <root url="jar://$PROJECT_DIR$/lib/common/commons-codec-1.6.jar!/" />
-      <root url="jar://$PROJECT_DIR$/lib/common/commons-logging-1.1.3.jar!/" />
-      <root url="jar://$PROJECT_DIR$/lib/common/fluent-hc-4.3.4.jar!/" />
-      <root url="jar://$PROJECT_DIR$/lib/common/httpclient-4.3.4.jar!/" />
-      <root url="jar://$PROJECT_DIR$/lib/common/httpclient-cache-4.3.4.jar!/" />
-      <root url="jar://$PROJECT_DIR$/lib/common/httpmime-4.3.4.jar!/" />
+      <root url="jar://$PROJECT_DIR$/lib/common/commons-codec-1.9.jar!/" />
+      <root url="jar://$PROJECT_DIR$/lib/common/commons-logging-1.2.jar!/" />
+      <root url="jar://$PROJECT_DIR$/lib/common/fluent-hc-4.5.3.jar!/" />
+      <root url="jar://$PROJECT_DIR$/lib/common/httpclient-4.5.3.jar!/" />
+      <root url="jar://$PROJECT_DIR$/lib/common/httpclient-cache-4.5.3.jar!/" />
+      <root url="jar://$PROJECT_DIR$/lib/common/httpcore-4.4.6.jar!/" />
+      <root url="jar://$PROJECT_DIR$/lib/common/httpmime-4.5.3.jar!/" />
     </CLASSES>
     <JAVADOC />
     <SOURCES>
-      <root url="jar://$PROJECT_DIR$/lib/common/src/commons-codec-1.6-src.zip!/commons-codec-1.6-src/src/main/java" />
-      <root url="jar://$PROJECT_DIR$/lib/common/src/commons-codec-1.6-src.zip!/commons-codec-1.6-src/src/test/java" />
-      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.3.4-src.zip!/httpcomponents-client-4.3.4/httpmime/src/main/java" />
-      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.3.4-src.zip!/httpcomponents-client-4.3.4/httpmime/src/main/java-deprecated" />
-      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.3.4-src.zip!/httpcomponents-client-4.3.4/httpmime/src/test/java" />
-      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.3.4-src.zip!/httpcomponents-client-4.3.4/httpmime/src/examples" />
-      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.3.4-src.zip!/httpcomponents-client-4.3.4/fluent-hc/src/main/java" />
-      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.3.4-src.zip!/httpcomponents-client-4.3.4/fluent-hc/src/test/java" />
-      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.3.4-src.zip!/httpcomponents-client-4.3.4/fluent-hc/src/examples" />
-      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.3.4-src.zip!/httpcomponents-client-4.3.4/httpclient/src/main/java" />
-      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.3.4-src.zip!/httpcomponents-client-4.3.4/httpclient/src/main/java-deprecated" />
-      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.3.4-src.zip!/httpcomponents-client-4.3.4/httpclient/src/test/java" />
-      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.3.4-src.zip!/httpcomponents-client-4.3.4/httpclient/src/examples" />
-      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.3.4-src.zip!/httpcomponents-client-4.3.4/httpclient-win/src/main/java" />
-      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.3.4-src.zip!/httpcomponents-client-4.3.4/httpclient-osgi/src/main/java" />
-      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.3.4-src.zip!/httpcomponents-client-4.3.4/httpclient-osgi/src/test/java" />
-      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.3.4-src.zip!/httpcomponents-client-4.3.4/httpclient-cache/src/main/java" />
-      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.3.4-src.zip!/httpcomponents-client-4.3.4/httpclient-cache/src/main/java-deprecated" />
-      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.3.4-src.zip!/httpcomponents-client-4.3.4/httpclient-cache/src/test/java" />
+      <root url="jar://$PROJECT_DIR$/lib/common/src/commons-codec-1.9-src.zip!/commons-codec-1.9-src/src/main/java" />
+      <root url="jar://$PROJECT_DIR$/lib/common/src/commons-codec-1.9-src.zip!/commons-codec-1.9-src/src/test/java" />
+      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.5.3-src.zip!/httpcomponents-client-4.5.3/fluent-hc/src/examples" />
+      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.5.3-src.zip!/httpcomponents-client-4.5.3/fluent-hc/src/main/java" />
+      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.5.3-src.zip!/httpcomponents-client-4.5.3/fluent-hc/src/test/java" />
+      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.5.3-src.zip!/httpcomponents-client-4.5.3/httpclient-cache/src/main/java" />
+      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.5.3-src.zip!/httpcomponents-client-4.5.3/httpclient-cache/src/main/java-deprecated" />
+      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.5.3-src.zip!/httpcomponents-client-4.5.3/httpclient-cache/src/test/java" />
+      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.5.3-src.zip!/httpcomponents-client-4.5.3/httpclient-osgi/src/main/java" />
+      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.5.3-src.zip!/httpcomponents-client-4.5.3/httpclient-osgi/src/test/java" />
+      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.5.3-src.zip!/httpcomponents-client-4.5.3/httpclient-win/src/examples" />
+      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.5.3-src.zip!/httpcomponents-client-4.5.3/httpclient-win/src/main/java" />
+      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.5.3-src.zip!/httpcomponents-client-4.5.3/httpclient-win/src/test/java" />
+      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.5.3-src.zip!/httpcomponents-client-4.5.3/httpclient/src/examples" />
+      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.5.3-src.zip!/httpcomponents-client-4.5.3/httpclient/src/main/java" />
+      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.5.3-src.zip!/httpcomponents-client-4.5.3/httpclient/src/main/java-deprecated" />
+      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.5.3-src.zip!/httpcomponents-client-4.5.3/httpclient/src/test/java" />
+      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.5.3-src.zip!/httpcomponents-client-4.5.3/httpmime/src/examples" />
+      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.5.3-src.zip!/httpcomponents-client-4.5.3/httpmime/src/main/java" />
+      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.5.3-src.zip!/httpcomponents-client-4.5.3/httpmime/src/main/java-deprecated" />
+      <root url="jar://$PROJECT_DIR$/lib/common/src/httpcomponents-client-4.5.3-src.zip!/httpcomponents-client-4.5.3/httpmime/src/test/java" />
     </SOURCES>
   </library>
 </component>
\ No newline at end of file
index 48891e45f3d3605e1a66e20803a686dcabbc43dc..d31ce3e1961fe62f2539efe3984ca8d4383538c5 100644 (file)
@@ -20,7 +20,7 @@
     <orderEntry type="library" name="TeamCity Agent" level="project" />
     <orderEntry type="library" name="jsch" level="project" />
     <orderEntry type="library" name="jgit" level="project" />
-    <orderEntry type="library" name="commons-codec-1.3" level="project" />
     <orderEntry type="library" name="slf4j-api-1.7.5" level="project" />
+    <orderEntry type="library" name="commons-codec" level="project" />
   </component>
 </module>
\ No newline at end of file
index 197215c1866fbc457571e2348362ae08bbfdd20b..1c3e7a1ffd8b3d8300930d558201e9d2cedae7e6 100644 (file)
@@ -16,4 +16,5 @@
   <bean id="mirrorManager" class="jetbrains.buildServer.buildTriggers.vcs.git.MirrorManagerImpl"/>
   <bean class="jetbrains.buildServer.buildTriggers.vcs.git.agent.SshKeyManagerProviderImpl"/>
   <bean class="jetbrains.buildServer.buildTriggers.vcs.git.agent.GitMetaFactoryImpl"/>
+  <bean class="jetbrains.buildServer.buildTriggers.vcs.git.agent.GCIdleTask"/>
 </beans>
index 36d1f7b04f489193d71bf2cebbc959e637b5ba31..62ce0ff3933021c14c2fd9b18a912b783c38e08d 100644 (file)
@@ -2,7 +2,7 @@
 
 if ""%1"" == ""erase"" goto erase
 
-java -cp {CREDENTIALS_CLASSPATH} {CREDENTIALS_CLASS} %*
+{JAVA} -cp {CREDENTIALS_CLASSPATH} {CREDENTIALS_CLASS} %*
 goto end
 
 :erase
index aa2a01894ba16b93b6c4cfba7d19bbdbe709f32c..01ac103200fd741b297e8b6ddbbd06d58ee8b3ec 100644 (file)
@@ -5,4 +5,4 @@ if [ "$1" = "erase" ]; then
   exit;
 fi
 
-java -cp '{CREDENTIALS_CLASSPATH}' {CREDENTIALS_CLASS} $*
\ No newline at end of file
+{JAVA} -cp '{CREDENTIALS_CLASSPATH}' {CREDENTIALS_CLASS} $*
\ No newline at end of file
index 1e85ef9e6687bb6cbf4e819ccf7989c259f29843..6d2f37e098d9e9115d2ab9aefdc50e3b1251f339 100644 (file)
@@ -55,5 +55,34 @@ public interface AgentPluginConfig extends PluginConfig {
 
   boolean isFailOnCleanCheckout();
 
+  boolean isFetchTags();
+
   boolean isCredHelperMatchesAllUrls();
+
+  @NotNull
+  GitProgressMode getGitProgressMode();
+
+  boolean isExcludeUsernameFromHttpUrl();
+
+  boolean isCleanCredHelperScript();
+
+  boolean isProvideCredHelper();
+
+  /**
+   * Defines how progress output from git commands is written into build log
+   */
+  enum GitProgressMode {
+    /**
+     * Don't write progress into build log
+     */
+    NONE,
+    /**
+     * Write progress as verbose messages
+     */
+    DEBUG,
+    /**
+     * Write progress as normal messages
+     */
+    NORMAL
+  }
 }
index 32f4b5c4249d184b0c611532a5494bf7bb435fa5..704c936f32a1a706fa2309da5261ac66f87b5a0d 100644 (file)
@@ -25,9 +25,12 @@ import org.jetbrains.annotations.Nullable;
 public class BuildContext implements Context {
 
   private final AgentRunningBuild myBuild;
+  private final AgentPluginConfig myConfig;
 
-  public BuildContext(@NotNull AgentRunningBuild build) {
+  public BuildContext(@NotNull AgentRunningBuild build,
+                      @NotNull AgentPluginConfig config) {
     myBuild = build;
+    myConfig = config;
   }
 
   @Nullable
@@ -43,4 +46,9 @@ public class BuildContext implements Context {
       return value;
     return null;
   }
+
+  @Override
+  public boolean isProvideCredHelper() {
+    return myConfig.isProvideCredHelper();
+  }
 }
index 47726d2e200c8b637d07467bfc7792d827ea942c..c214f8fb9374e95cc5a2945c5082d7e04888ca10 100644 (file)
@@ -27,4 +27,6 @@ public interface Context {
   @Nullable
   String getSshMacType();
 
+  boolean isProvideCredHelper();
+
 }
diff --git a/git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/GCIdleTask.java b/git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/GCIdleTask.java
new file mode 100644 (file)
index 0000000..f56c457
--- /dev/null
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2000-2017 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;
+
+import jetbrains.buildServer.agent.*;
+import jetbrains.buildServer.buildTriggers.vcs.git.MirrorManager;
+import jetbrains.buildServer.log.Loggers;
+import jetbrains.buildServer.util.Disposable;
+import jetbrains.buildServer.util.EventDispatcher;
+import jetbrains.buildServer.util.NamedThreadFactory;
+import org.eclipse.jgit.lib.RepositoryBuilder;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Runs 'git gc' when agent is idle
+ */
+public class GCIdleTask implements AgentIdleTasks.Task {
+
+  private final BuildAgentConfiguration myAgentConfig;
+  private final MirrorManager myMirrorManager;
+  //nano timestamp of the last build or of the agent start, used to delay git gc
+  private final AtomicLong myBuildFinishTime = new AtomicLong(-1);
+  //name of the mirror dir -> nano timestamp of the previous git gc
+  private final ConcurrentMap<String, Long> myGcTimestamp = new ConcurrentHashMap<String, Long>();
+  //ref containing the thread executing 'git gc' or null if 'git gc' is not running
+  private final AtomicReference<Thread> myGcThread = new AtomicReference<Thread>();
+
+  public GCIdleTask(@NotNull EventDispatcher<AgentLifeCycleListener> events,
+                    @NotNull AgentIdleTasks idleTasks,
+                    @NotNull BuildAgentConfiguration agentConfig,
+                    @NotNull MirrorManager mirrorManager) {
+    myAgentConfig = agentConfig;
+    myMirrorManager = mirrorManager;
+    events.addListener(new AgentLifeCycleAdapter() {
+      @Override
+      public void buildFinished(@NotNull AgentRunningBuild build, @NotNull BuildFinishedStatus buildStatus) {
+        myBuildFinishTime.set(System.nanoTime());
+      }
+      @Override
+      public void agentStarted(@NotNull BuildAgent agent) {
+        myBuildFinishTime.set(System.nanoTime());
+      }
+
+      @Override
+      public void buildStarted(@NotNull AgentRunningBuild runningBuild) {
+        Thread thread = myGcThread.get();
+        if (thread != null)
+          thread.interrupt();
+      }
+    });
+    idleTasks.addRecurringTask(this);
+  }
+
+
+  @NotNull
+  @Override
+  public String getName() {
+    return "git gc";
+  }
+
+
+  @Override
+  public void execute(@NotNull InterruptState interruptState) {
+    if (!isEnabled())
+      return;
+    if (!isDelayPassed())
+      return;
+    long t0 = System.currentTimeMillis();
+    try {
+      myGcThread.set(Thread.currentThread());
+      Loggers.VCS.debug("Start git gc");
+      runGc(interruptState);
+    } catch (Exception e) {
+      Loggers.VCS.debug("Finished git gc in " + (System.currentTimeMillis() - t0) + "ms");
+    } finally {
+      myGcThread.set(null);
+    }
+  }
+
+
+  private void runGc(@NotNull InterruptState interruptState) {
+    if (interruptState.isInterrupted())
+      return;
+    List<File> mirrors = listMirrors();
+    Collections.shuffle(mirrors);
+    for (File mirror : mirrors) {
+      if (interruptState.isInterrupted())
+        return;
+      Long previousGC = myGcTimestamp.get(mirror.getName());
+      if (previousGC != null && TimeUnit.NANOSECONDS.toHours(System.nanoTime() - previousGC) < getDelaySinceLastGCHours())
+        continue;
+      long t0 = System.currentTimeMillis();
+      String path = mirror.getAbsolutePath();
+      Loggers.VCS.debug("Run git gc in " + path);
+      Disposable name = NamedThreadFactory.patchThreadName("Run git gc in " + path);
+      try {
+        new NativeGitFacade("git", GitProgressLogger.NO_OP, mirror).gc().call();
+        myGcTimestamp.put(mirror.getName(), System.nanoTime());
+      } catch (Exception e) {
+        Loggers.VCS.warnAndDebugDetails("Error while running git gc in " + path, e);
+      } finally {
+        name.dispose();
+        Loggers.VCS.debug("Finished git gc in " + path + " in " + (System.currentTimeMillis() - t0) + "ms");
+      }
+    }
+  }
+
+
+  @NotNull
+  private List<File> listMirrors() {
+    File mirrorsDir = myMirrorManager.getBaseMirrorsDir();
+    File[] mirrors = mirrorsDir.listFiles();
+    if (mirrors == null)
+      return Collections.emptyList();
+    List<File> result = new ArrayList<File>();
+    for (File f : mirrors) {
+      if (isGitRepo(f))
+        result.add(f);
+    }
+    return result;
+  }
+
+
+  private boolean isGitRepo(@NotNull File gitDir) {
+    try {
+      new RepositoryBuilder().setGitDir(gitDir).setMustExist(true).build();
+      return true;
+    } catch (IOException e) {
+      return false;
+    }
+  }
+
+
+  private boolean isEnabled() {
+    return "true".equals(myAgentConfig.getConfigurationParameters().get("teamcity.git.idleGcEnabled"));
+  }
+
+
+  private boolean isDelayPassed() {
+    long buildFinishTime = myBuildFinishTime.get();
+    if (buildFinishTime == -1)
+      return false;
+    return TimeUnit.NANOSECONDS.toMinutes(System.nanoTime() - buildFinishTime) >= getDelaySinceLastBuildMinutes();
+  }
+
+
+  private long getDelaySinceLastBuildMinutes() {
+    return getLongParameter("teamcity.git.idleGcDelayMinutes", 30);
+  }
+
+
+  private long getDelaySinceLastGCHours() {
+    return getLongParameter("teamcity.git.idleGcRateHours", 12);
+  }
+
+
+  private long getLongParameter(@NotNull String name, long defaultValue) {
+    String value = myAgentConfig.getConfigurationParameters().get(name);
+    if (value == null) {
+      return defaultValue;
+    } else {
+      try {
+        return Long.parseLong(value);
+      } catch (NumberFormatException e) {
+        return defaultValue;
+      }
+    }
+  }
+}
index 8d0c493291e58ed411de2f99aff2a8f5db242686..0c560d00bd6d1bfc1ee105e35c5ee113ecbc35a7 100644 (file)
@@ -26,18 +26,15 @@ import jetbrains.buildServer.agent.vcs.UpdatePolicy;
 import jetbrains.buildServer.buildTriggers.vcs.git.Constants;
 import jetbrains.buildServer.buildTriggers.vcs.git.GitVcsRoot;
 import jetbrains.buildServer.buildTriggers.vcs.git.MirrorManager;
-import jetbrains.buildServer.vcs.CheckoutRules;
-import jetbrains.buildServer.vcs.IncludeRule;
-import jetbrains.buildServer.vcs.VcsException;
-import jetbrains.buildServer.vcs.VcsRoot;
+import jetbrains.buildServer.vcs.*;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import java.io.File;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicLong;
 
 /**
  * The agent support for VCS.
@@ -51,6 +48,15 @@ public class GitAgentVcsSupport extends AgentVcsSupport implements UpdateByCheck
   private final MirrorManager myMirrorManager;
   private final GitMetaFactory myGitMetaFactory;
 
+  //The canCheckout() method should check that roots are not checked out in the same dir (TW-49786).
+  //To do that we need to create AgentPluginConfig for each VCS root which involves 'git version'
+  //command execution. Since we don't have a dedicated API for checking several roots, every root
+  //is checked with all other roots. In order to avoid running n^2 'git version' commands configs
+  //are cached for the build. Cache is reset when we get a new build.
+  private final AtomicLong myConfigsCacheBuildId = new AtomicLong(-1); //buildId for which configs are cached
+  private final ConcurrentMap<VcsRoot, AgentPluginConfig> myConfigsCache = new ConcurrentHashMap<VcsRoot, AgentPluginConfig>();//cached config per root
+  private final ConcurrentMap<VcsRoot, VcsException> myConfigErrorsCache = new ConcurrentHashMap<VcsRoot, VcsException>();//cached error thrown during config creation per root
+
   public GitAgentVcsSupport(@NotNull FS fs,
                             @NotNull SmartDirectoryCleaner directoryCleaner,
                             @NotNull GitAgentSSHService sshService,
@@ -88,8 +94,8 @@ public class GitAgentVcsSupport extends AgentVcsSupport implements UpdateByCheck
                             boolean cleanCheckoutRequested) throws VcsException {
     AgentPluginConfig config = myConfigFactory.createConfig(build, root);
     Map<String, String> env = getGitCommandEnv(config, build);
-    GitFactory gitFactory = myGitMetaFactory.createFactory(mySshService, config, getLogger(build), build.getBuildTempDirectory(), env, new BuildContext(build));
-    Pair<CheckoutMode, File> targetDirAndMode = getTargetDirAndMode(config, root, rules, checkoutDirectory);
+    GitFactory gitFactory = myGitMetaFactory.createFactory(mySshService, config, getLogger(build, config), build.getBuildTempDirectory(), env, new BuildContext(build, config));
+    Pair<CheckoutMode, File> targetDirAndMode = getTargetDirAndMode(config, rules, checkoutDirectory);
     CheckoutMode mode = targetDirAndMode.first;
     File targetDir = targetDirAndMode.second;
     Updater updater;
@@ -119,17 +125,19 @@ public class GitAgentVcsSupport extends AgentVcsSupport implements UpdateByCheck
   public AgentCheckoutAbility canCheckout(@NotNull final VcsRoot vcsRoot, @NotNull CheckoutRules checkoutRules, @NotNull final AgentRunningBuild build) {
     AgentPluginConfig config;
     try {
-      config = myConfigFactory.createConfig(build, vcsRoot);
+      config = getAndCacheConfig(build, vcsRoot);
     } catch (VcsException e) {
       return AgentCheckoutAbility.noVcsClientOnAgent(e.getMessage());
     }
 
-    try {
-      boolean validSparseCheckout = canUseSparseCheckout(config) && getSingleTargetDirForSparseCheckout(checkoutRules) != null;
-      if (!validSparseCheckout)
-        validateCheckoutRules(vcsRoot, checkoutRules);
-    } catch (VcsException e) {
-      return AgentCheckoutAbility.notSupportedCheckoutRules(e.getMessage());
+    Pair<CheckoutMode, String> pathAndMode = getTargetPathAndMode(checkoutRules);
+    String targetDir = pathAndMode.second;
+    if (targetDir == null) {
+      return AgentCheckoutAbility.notSupportedCheckoutRules("Unsupported rules for agent-side checkout: " + checkoutRules.getAsString());
+    }
+
+    if (pathAndMode.first == CheckoutMode.SPARSE_CHECKOUT && !canUseSparseCheckout(config)) {
+      return AgentCheckoutAbility.notSupportedCheckoutRules("Cannot perform sparse checkout using git " + config.getGitExec().getVersion());
     }
 
     try {
@@ -139,79 +147,126 @@ public class GitAgentVcsSupport extends AgentVcsSupport implements UpdateByCheck
       return AgentCheckoutAbility.canNotCheckout(e.getMessage());
     }
 
+    List<VcsRootEntry> gitEntries = getGitRootEntries(build);
+    if (gitEntries.size() > 1) {
+      for (VcsRootEntry entry : gitEntries) {
+        VcsRoot otherRoot = entry.getVcsRoot();
+        if (vcsRoot.equals(otherRoot))
+          continue;
+
+        AgentPluginConfig otherConfig;
+        try {
+          otherConfig = getAndCacheConfig(build, otherRoot);
+        } catch (VcsException e) {
+          continue;//appropriate reason will be returned during otherRoot check
+        }
+        Pair<CheckoutMode, String> otherPathAndMode = getTargetPathAndMode(entry.getCheckoutRules());
+        if (otherPathAndMode.first == CheckoutMode.SPARSE_CHECKOUT && !canUseSparseCheckout(otherConfig)) {
+          continue;//appropriate reason will be returned during otherRoot check
+        }
+        String entryPath = otherPathAndMode.second;
+        if (targetDir.equals(entryPath))
+          return AgentCheckoutAbility.canNotCheckout("Cannot checkout VCS root '" + vcsRoot.getName() + "' into the same directory as VCS root '" + otherRoot.getName() + "'");
+      }
+    }
+
     return AgentCheckoutAbility.canCheckout();
   }
 
-  @NotNull
-  private GitBuildProgressLogger getLogger(@NotNull AgentRunningBuild build) {
-    return new GitBuildProgressLogger(build.getBuildLogger().getFlowLogger("-1"));
+
+  private boolean isRequireSparseCheckout(@NotNull CheckoutRules rules) {
+    if (!rules.getExcludeRules().isEmpty())
+      return true;
+    List<IncludeRule> includeRules = rules.getRootIncludeRules();
+    if (includeRules.isEmpty() || includeRules.size() > 1)
+      return true;
+    IncludeRule rule = includeRules.get(0);
+    return !"".equals(rule.getFrom()); //rule of form +:.=>dir doesn't require sparse checkout ('.' is transformed into empty string)
   }
 
 
-  /**
-   * Check if specified checkout rules are supported
-   * @param root root for which rules are checked
-   * @param rules rules to check
-   * @throws VcsException rules are not supported
-   */
-  private void validateCheckoutRules(@NotNull final VcsRoot root, @NotNull final CheckoutRules rules) throws VcsException {
-    if (rules.getExcludeRules().size() != 0) {
-      throw new VcsException("Exclude rules are not supported for agent checkout for the git (" + rules.getExcludeRules().size() +
-                             " rule(s) detected) for VCS Root '" + root.getName() + "'");
-    }
-    if (rules.getIncludeRules().size() > 1) {
-      throw new VcsException("At most one include rule is supported for agent checkout for the git (" + rules.getIncludeRules().size() +
-                             " rule(s) detected) for VCS Root '" + root.getName() + "'");
-    }
-    if (rules.getIncludeRules().size() == 1) {
-      IncludeRule ir = rules.getIncludeRules().get(0);
-      if (!".".equals(ir.getFrom()) && ir.getFrom().length() != 0) {
-        throw new VcsException("Agent checkout for the git supports only include rule of form '. => subdir', rule '" + ir.toDescriptiveString() +
-                               "' for VCS Root '" + root.getName() + "' is not supported");
-      }
+  @NotNull
+  private List<VcsRootEntry> getGitRootEntries(@NotNull AgentRunningBuild build) {
+    List<VcsRootEntry> result = new ArrayList<VcsRootEntry>();
+    for (VcsRootEntry entry : build.getVcsRootEntries()) {
+      if (Constants.VCS_NAME.equals(entry.getVcsRoot().getVcsName()))
+        result.add(entry);
     }
+    return result;
   }
 
 
-  /**
-   * Get the destination directory creating it if it is missing
-   * @param root VCS root
-   * @param rules checkout rules
-   * @param checkoutDirectory checkout directory for the build
-   * @return the directory where vcs root should be checked out according to checkout rules
-   * @throws VcsException if the directory could not be located or created
-   */
-  private File getTargetDir(@NotNull final VcsRoot root, @NotNull final CheckoutRules rules, @NotNull final File checkoutDirectory) throws VcsException {
-    String path = rules.map("");
-    if (path == null)
-      throw new VcsException("The root path could not be mapped for VCS root '" + root.getName() + "'");
-
-    File directory = path.length() == 0 ? checkoutDirectory : new File(checkoutDirectory, path.replace('/', File.separatorChar));
-    if (!directory.exists()) {
-      //noinspection ResultOfMethodCallIgnored
-      directory.mkdirs();
-      if (!directory.exists())
-        throw new VcsException("The destination directory '" + directory + "' could not be created.");
-    }
-    return directory;
+  @NotNull
+  private GitBuildProgressLogger getLogger(@NotNull AgentRunningBuild build, @NotNull AgentPluginConfig config) {
+    return new GitBuildProgressLogger(build.getBuildLogger().getFlowLogger("-1"), config.getGitProgressMode());
   }
 
 
   @NotNull
   private Pair<CheckoutMode, File> getTargetDirAndMode(@NotNull AgentPluginConfig config,
-                                                       @NotNull VcsRoot root,
                                                        @NotNull CheckoutRules rules,
                                                        @NotNull File checkoutDir) throws VcsException {
-    if (canUseSparseCheckout(config)) {
-      String targetDir = getSingleTargetDirForSparseCheckout(rules);
-      if (targetDir != null) {
-        return Pair.create(CheckoutMode.SPARSE_CHECKOUT, new File(checkoutDir, targetDir));
+    Pair<CheckoutMode, String> pathAndMode = getTargetPathAndMode(rules);
+    String path = pathAndMode.second;
+    if (path == null) {
+      throw new VcsException("Unsupported checkout rules for agent-side checkout: " + rules.getAsString());
+    }
+
+    boolean canUseSparseCheckout = canUseSparseCheckout(config);
+    if (pathAndMode.first == CheckoutMode.SPARSE_CHECKOUT && !canUseSparseCheckout) {
+      throw new VcsException("Cannot perform sparse checkout using git " + config.getGitExec().getVersion());
+    }
+
+    File targetDir = path.length() == 0 ? checkoutDir : new File(checkoutDir, path.replace('/', File.separatorChar));
+    if (!targetDir.exists()) {
+      //noinspection ResultOfMethodCallIgnored
+      targetDir.mkdirs();
+      if (!targetDir.exists())
+        throw new VcsException("Cannot create destination directory '" + targetDir + "'");
+    }
+
+    //Use sparse checkout mode if we can, without that switch from rules requiring sparse checkout
+    //to simple rules (e.g. to CheckoutRules.DEFAULT) doesn't work (run AgentSideSparseCheckoutTest.
+    //update_files_after_switching_to_default_rules). Probably it is a rare case when we checked out
+    //a repository using sparse checkout and then cannot use sparse checkout in the next build.
+    CheckoutMode mode = canUseSparseCheckout ? CheckoutMode.SPARSE_CHECKOUT : pathAndMode.first;
+    return Pair.create(mode, targetDir);
+  }
+
+
+  @NotNull
+  private AgentPluginConfig getAndCacheConfig(@NotNull AgentRunningBuild build, @NotNull VcsRoot root) throws VcsException {
+    //reset cache if we get a new build
+    if (build.getBuildId() != myConfigsCacheBuildId.get()) {
+      myConfigsCacheBuildId.set(build.getBuildId());
+      myConfigsCache.clear();
+      myConfigErrorsCache.clear();
+    }
+
+    AgentPluginConfig result = myConfigsCache.get(root);
+    if (result == null) {
+      VcsException error = myConfigErrorsCache.get(root);
+      if (error != null)
+        throw error;
+      try {
+        result = myConfigFactory.createConfig(build, root);
+      } catch (VcsException e) {
+        myConfigErrorsCache.put(root, e);
+        throw e;
       }
+      myConfigsCache.put(root, result);
     }
+    return result;
+  }
 
-    validateCheckoutRules(root, rules);
-    File targetDir = getTargetDir(root, rules, checkoutDir);
-    return Pair.create(CheckoutMode.MAP_REPO_TO_DIR, targetDir);
+
+  @NotNull
+  private Pair<CheckoutMode, String> getTargetPathAndMode(@NotNull CheckoutRules rules) {
+    if (isRequireSparseCheckout(rules)) {
+      return Pair.create(CheckoutMode.SPARSE_CHECKOUT, getSingleTargetDirForSparseCheckout(rules));
+    } else {
+      return Pair.create(CheckoutMode.MAP_REPO_TO_DIR, rules.map(""));
+    }
   }
 
   private boolean canUseSparseCheckout(@NotNull AgentPluginConfig config) {
index f99df18f6d694e265a56681cd25b4958f03d2287..0ca618fa0be6ea610de7f62c9a6377ce7287ae7f 100644 (file)
 package jetbrains.buildServer.buildTriggers.vcs.git.agent;
 
 import jetbrains.buildServer.agent.BuildProgressLogger;
+import jetbrains.buildServer.messages.BuildMessage1;
+import jetbrains.buildServer.messages.DefaultMessagesInfo;
 import org.jetbrains.annotations.NotNull;
 
 import java.util.concurrent.atomic.AtomicInteger;
 
 public class GitBuildProgressLogger implements GitProgressLogger {
 
+  public static final String GIT_PROGRESS_ACTIVITY = "CUSTOM_GIT_PROGRESS";
   private final BuildProgressLogger myLogger;
   private final AtomicInteger myBlockMessageCount = new AtomicInteger(0);
+  private final AgentPluginConfig.GitProgressMode myProgressMode;
 
-  public GitBuildProgressLogger(@NotNull BuildProgressLogger logger) {
+  public GitBuildProgressLogger(@NotNull BuildProgressLogger logger,
+                                @NotNull AgentPluginConfig.GitProgressMode progressMode) {
     myLogger = logger;
+    myProgressMode = progressMode;
   }
 
   public void openBlock(@NotNull String name) {
     myBlockMessageCount.set(0);
-    myLogger.activityStarted(name, "CUSTOM_GIT_PROGRESS");
+    if (myProgressMode == AgentPluginConfig.GitProgressMode.NONE) {
+      //if progress should not be written to build log do not open block; write it
+      //as a regular message instead, because teamcity doesn't show empty blocks
+      //with no messages inside
+      myLogger.message(name);
+    } else {
+      myLogger.activityStarted(name, GIT_PROGRESS_ACTIVITY);
+    }
   }
 
   public void message(@NotNull String message) {
@@ -40,9 +53,34 @@ public class GitBuildProgressLogger implements GitProgressLogger {
     myLogger.message(message);
   }
 
+  @Override
+  public void progressMessage(@NotNull String message) {
+    myBlockMessageCount.incrementAndGet();
+    switch (myProgressMode) {
+      case NONE:
+        return;
+      case DEBUG:
+        myLogger.logMessage(DefaultMessagesInfo.internalize(createBuildLogMessage(message)));
+        return;
+      case NORMAL:
+        myLogger.logMessage(createBuildLogMessage(message));
+    }
+  }
+
   public void closeBlock(@NotNull String name) {
-    if (myBlockMessageCount.get() == 0)
-      myLogger.message("");
-    myLogger.activityFinished(name, "CUSTOM_GIT_PROGRESS");
+    //we need to close block only if progress was written to build log
+    if (myProgressMode != AgentPluginConfig.GitProgressMode.NONE) {
+      if (myBlockMessageCount.get() == 0)
+        myLogger.message("");
+      myLogger.activityFinished(name, GIT_PROGRESS_ACTIVITY);
+    }
+  }
+
+
+  @NotNull
+  private BuildMessage1 createBuildLogMessage(@NotNull String message) {
+    //write git output containing '%' as progress messages to show them in UI;
+    //lines without '%' include e.g. fetched refs names, it doesn't make sense to show them in UI
+    return message.contains("%") ? DefaultMessagesInfo.createProgressMessage(message) : DefaultMessagesInfo.createTextMessage(message);
   }
 }
index 7cf2e0fceab44fdb4dad8efe12d93449ff840755..ecd8886f081932d9ff5e9e041c4d8ac3d8c94ffc 100644 (file)
@@ -77,7 +77,7 @@ public class GitCommandLine extends GeneralCommandLine {
 
   public ExecResult run(@NotNull GitCommandSettings settings) throws VcsException {
     AuthSettings authSettings = settings.getAuthSettings();
-    if (!getParametersList().getParametersString().contains("credential.helper") && !myGitVersion.isLessThan(UpdaterImpl.EMPTY_CRED_HELPER)) {
+    if (myCtx.isProvideCredHelper() && !getParametersList().getParametersString().contains("credential.helper") && !myGitVersion.isLessThan(UpdaterImpl.EMPTY_CRED_HELPER)) {
       //Disable credential helper if it wasn't specified by us, as default
       //helper can require a user input. Do that even if our repository doesn't
       //require any auth, auth can be required by submodules or git lfs.
@@ -178,11 +178,6 @@ public class GitCommandLine extends GeneralCommandLine {
     setEnvParams(newParams);
   }
 
-  @NotNull
-  public GitProgressLogger getLogger() {
-    return myLogger;
-  }
-
   @NotNull
   public ByteArrayOutputStream createStderrBuffer() {
     LineAwareByteArrayOutputStream buffer = new LineAwareByteArrayOutputStream(Charset.forName("UTF-8"), new GitProgressListener(myLogger));
index c3d545d4fadb340aee950b514d48f959e0094a15..fbd44020172860e1ed6f42831afccfceae032aa2 100644 (file)
@@ -24,13 +24,17 @@ public interface GitProgressLogger {
 
   void message(@NotNull String message);
 
+  void progressMessage(@NotNull String message);
+
   void closeBlock(@NotNull String name);
 
-  static GitProgressLogger NO_OP = new GitProgressLogger() {
+  GitProgressLogger NO_OP = new GitProgressLogger() {
     public void openBlock(@NotNull final String name) {
     }
     public void message(@NotNull final String message) {
     }
+    public void progressMessage(@NotNull final String message) {
+    }
     public void closeBlock(@NotNull final String name) {
     }
   };
index d72c1adb611341516e8da11259051bab0ac108f5..1c182cf1ba00b54611faa2f7927f97606e014089 100644 (file)
@@ -77,12 +77,19 @@ public class NativeGitFacade implements GitFacade {
   }
 
   public NativeGitFacade(@NotNull String gitPath, @NotNull GitProgressLogger logger) {
+    this(gitPath, logger, new File("."));
+  }
+
+
+  public NativeGitFacade(@NotNull String gitPath,
+                         @NotNull GitProgressLogger logger,
+                         @NotNull File repositoryDir) {
     mySsh = null;
     myTmpDir = new File(FileUtil.getTempDirectory());
     myScriptGen = makeScriptGen();
     myGitPath = gitPath;
     myGitVersion = GitVersion.MIN;
-    myRepositoryDir = new File(".");
+    myRepositoryDir = repositoryDir;
     myDeleteTempFiles = true;
     myLogger = logger;
     myGitExec = null;
index 57bf0c834b28715ceb0625246bb312e6268d34c6..437250a22dd350535b3af657b5db005bc6040f55 100644 (file)
@@ -32,4 +32,9 @@ public class NoBuildContext implements Context {
   public String getSshMacType() {
     return null;
   }
+
+  @Override
+  public boolean isProvideCredHelper() {
+    return false;
+  }
 }
index 6e981759365d3e14d225ec70bb3346dfa50a407a..8b1206e5d968c53a3b1b7c73f4e27aa68c244a8d 100644 (file)
@@ -47,6 +47,10 @@ public class PluginConfigImpl implements AgentPluginConfig {
   public static final String USE_SPARSE_CHECKOUT = "teamcity.git.useSparseCheckout";
   public static final String USE_BUILD_ENV = "teamcity.git.useBuildEnv";
   public static final String FETCH_ALL_HEADS = "teamcity.git.fetchAllHeads";
+  public static final String FETCH_TAGS = "teamcity.git.fetchTags";
+  public static final String EXCLUDE_USERNAME_FROM_HTTP_URL = "teamcity.git.excludeUsernameFromHttpUrl";
+  public static final String CLEAN_CRED_HELPER_SCRIPT = "teamcity.git.cleanCredHelperScript";
+  public static final String PROVIDE_CRED_HELPER = "teamcity.git.provideCredentialHelper";
 
   private final BuildAgentConfiguration myAgentConfig;
   private final AgentRunningBuild myBuild;
@@ -213,6 +217,12 @@ public class PluginConfigImpl implements AgentPluginConfig {
 
 
   @Override
+  public boolean isFetchTags() {
+    String value = myBuild.getSharedConfigParameters().get(FETCH_TAGS);
+    //by default tags are fetched
+    return !"false".equals(value);
+  }
+
   public boolean isCredHelperMatchesAllUrls() {
     //it looks to be safe to enable all urls matching by default because we did
     //a similar thing with ask-pass script: it provides password for any server
@@ -220,6 +230,37 @@ public class PluginConfigImpl implements AgentPluginConfig {
     return !"false".equals(value);
   }
 
+  @NotNull
+  @Override
+  public GitProgressMode getGitProgressMode() {
+    String value = myBuild.getSharedConfigParameters().get("teamcity.git.progressMode");
+    if (value == null)
+      return GitProgressMode.DEBUG;
+    try {
+      return GitProgressMode.valueOf(value.toUpperCase());
+    } catch (IllegalArgumentException e) {
+      return GitProgressMode.DEBUG;
+    }
+  }
+
+  @Override
+  public boolean isExcludeUsernameFromHttpUrl() {
+    String value = myBuild.getSharedConfigParameters().get(EXCLUDE_USERNAME_FROM_HTTP_URL);
+    return !"false".equals(value);
+  }
+
+  @Override
+  public boolean isCleanCredHelperScript() {
+    String value = myBuild.getSharedConfigParameters().get(CLEAN_CRED_HELPER_SCRIPT);
+    return !"false".equals(value);
+  }
+
+  @Override
+  public boolean isProvideCredHelper() {
+    String value = myBuild.getSharedConfigParameters().get(PROVIDE_CRED_HELPER);
+    return !"false".equals(value);
+  }
+
   private int parseTimeout(String valueFromBuild) {
     return parseTimeout(valueFromBuild, DEFAULT_IDLE_TIMEOUT);
   }
diff --git a/git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/RemoteRepositoryConfigurator.java b/git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/RemoteRepositoryConfigurator.java
new file mode 100644 (file)
index 0000000..f28f4dd
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2000-2017 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;
+
+import jetbrains.buildServer.buildTriggers.vcs.git.GitVcsRoot;
+import jetbrains.buildServer.util.StringUtil;
+import jetbrains.buildServer.vcs.VcsException;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryBuilder;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.transport.URIish;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+
+/**
+ * Configures remote repository
+ */
+public class RemoteRepositoryConfigurator {
+
+  private boolean myExcludeUsernameFromHttpUrls;
+  private File myGitDir;
+
+
+  public void setExcludeUsernameFromHttpUrls(boolean excludeUsernameFromHttpUrls) {
+    myExcludeUsernameFromHttpUrls = excludeUsernameFromHttpUrls;
+  }
+
+
+  public void setGitDir(@NotNull File gitDir) {
+    myGitDir = gitDir;
+  }
+
+
+  /**
+   * Configures and save the remote repository for specified VCS root
+   * @param root VCS root of interest
+   * @throws VcsException in case of any error
+   */
+  public void configure(@NotNull GitVcsRoot root) throws VcsException {
+    File gitDir = getGitDir();
+    Repository repository = null;
+    try {
+      repository = new RepositoryBuilder().setGitDir(gitDir).build();
+      StoredConfig config = repository.getConfig();
+      URIish fetchUrl = root.getRepositoryFetchURL();
+      String scheme = fetchUrl.getScheme();
+      String user = fetchUrl.getUser();
+      if (myExcludeUsernameFromHttpUrls && isHttp(scheme) && !StringUtil.isEmpty(user)) {
+        URIish fetchUrlNoUser = fetchUrl.setUser(null);
+        config.setString("remote", "origin", "url", fetchUrlNoUser.toString());
+        config.setString("credential", null, "username", user);
+      } else {
+        config.setString("remote", "origin", "url", fetchUrl.toString());
+        config.unset("credential", null, "username");
+      }
+      config.setString("remote", "origin", "fetch", "+refs/heads/*:refs/remotes/origin/*");
+      config.save();
+    } catch (Exception e) {
+      throw new VcsException("Error while configuring remote repository at " + gitDir, e);
+    } finally {
+      if (repository != null)
+        repository.close();
+    }
+  }
+
+
+  private boolean isHttp(@Nullable String scheme) {
+    return "http".equals(scheme) || "https".equals(scheme);
+  }
+
+
+  @NotNull
+  private File getGitDir() {
+    if (myGitDir != null)
+      return myGitDir;
+    throw new IllegalStateException("Git directory is not specified");
+  }
+}
index 4e57add35d6b7c9b70dbad2da62fb26d8516f8e4..03402ae0e2b459b3a1cd74b19d3af3c621c090d3 100644 (file)
@@ -65,6 +65,9 @@ public class UpdaterImpl implements Updater {
    * Git version supporting an empty credential helper - the only way to disable system/global/local cred helper
    */
   public final static GitVersion EMPTY_CRED_HELPER = new GitVersion(2, 9, 0);
+  /** Git version supporting [credential] section in config (the first version including a6fc9fd3f4b42cd97b5262026e18bd451c28ee3c) */
+  public final static GitVersion CREDENTIALS_SECTION_VERSION = new GitVersion(1, 7, 10);
+
   private static final int SILENT_TIMEOUT = 24 * 60 * 60; //24 hours
 
   protected final FS myFS;
@@ -80,6 +83,8 @@ public class UpdaterImpl implements Updater {
   private final CheckoutRules myRules;
   private final CheckoutMode myCheckoutMode;
   protected final MirrorManager myMirrorManager;
+  //remote repository refs, stored in field in order to not run 'git ls-remote' command twice
+  private Refs myRemoteRefs;
 
   public UpdaterImpl(@NotNull FS fs,
                      @NotNull AgentPluginConfig pluginConfig,
@@ -117,17 +122,25 @@ public class UpdaterImpl implements Updater {
 
 
   public void update() throws VcsException {
-    myLogger.message("Git version: " + myPluginConfig.getGitVersion());
+    String msg = "Git version: " + myPluginConfig.getGitVersion();
+    myLogger.message(msg);
+    LOG.info(msg);
     checkAuthMethodIsSupported();
     doUpdate();
   }
 
   protected void doUpdate() throws VcsException {
-    logStartUpdating();
-    initGitRepository();
-    removeRefLocks(new File(myTargetDirectory, ".git"));
-    doFetch();
-    updateSources();
+    String message = "Update checkout directory (" + myTargetDirectory.getAbsolutePath() + ")";
+    myLogger.activityStarted(message, GitBuildProgressLogger.GIT_PROGRESS_ACTIVITY);
+    try {
+      logStartUpdating();
+      initGitRepository();
+      removeRefLocks(new File(myTargetDirectory, ".git"));
+      doFetch();
+      updateSources();
+    } finally {
+      myLogger.activityFinished(message, GitBuildProgressLogger.GIT_PROGRESS_ACTIVITY);
+    }
   }
 
   private void logStartUpdating() {
@@ -136,33 +149,20 @@ public class UpdaterImpl implements Updater {
   }
 
 
-  /**
-   * Init .git in the target dir
-   * @return true if there was no fetch in the target dir before
-   * @throws VcsException in teh case of any problems
-   */
-  private boolean initGitRepository() throws VcsException {
-    boolean firstFetch = false;
+  private void initGitRepository() throws VcsException {
     if (!new File(myTargetDirectory, ".git").exists()) {
       initDirectory(false);
-      firstFetch = true;
     } else {
-      String remoteUrl = getRemoteUrl();
-      if (!remoteUrl.equals(myRoot.getRepositoryFetchURL().toString())) {
+      try {
+        configureRemoteUrl(new File(myTargetDirectory, ".git"));
+        setupExistingRepository();
+        configureSparseCheckout();
+      } catch (Exception e) {
+        LOG.warn("Do clean checkout due to errors while configure use of local mirrors", e);
         initDirectory(true);
-        firstFetch = true;
-      } else {
-        try {
-          setupExistingRepository();
-          configureSparseCheckout();
-        } catch (Exception e) {
-          LOG.warn("Do clean checkout due to errors while configure use of local mirrors", e);
-          initDirectory(true);
-          firstFetch = true;
-        }
       }
     }
-    return firstFetch;
+    removeOrphanedIdxFiles(new File(myTargetDirectory, ".git"));
   }
 
   protected void setupNewRepository() throws VcsException {
@@ -356,40 +356,51 @@ public class UpdaterImpl implements Updater {
     }
 
     Repository r = new RepositoryBuilder().setBare().setGitDir(getGitDir(repositoryDir)).build();
-    StoredConfig gitConfig = r.getConfig();
+    try {
+      StoredConfig gitConfig = r.getConfig();
 
-    Set<String> submodules = gitModules.getSubsections("submodule");
-    if (submodules.isEmpty()) {
-      Loggers.VCS.info("No submodule sections found in " + new File(repositoryDir, ".gitmodules").getCanonicalPath()
-                       + ", skip updating credentials");
-      return;
-    }
-    File modulesDir = new File(r.getDirectory(), Constants.MODULES);
-    for (String submoduleName : submodules) {
-      String url = gitModules.getString("submodule", submoduleName, "url");
-      Loggers.VCS.info("Update credentials for submodule with url " + url);
-      if (url == null || !isRequireAuth(url)) {
-        Loggers.VCS.info("Url " + url + " does not require authentication, skip updating credentials");
-        continue;
+      Set<String> submodules = gitModules.getSubsections("submodule");
+      if (submodules.isEmpty()) {
+        Loggers.VCS.info("No submodule sections found in " + new File(repositoryDir, ".gitmodules").getCanonicalPath()
+                         + ", skip updating credentials");
+        return;
       }
-      try {
-        URIish uri = new URIish(url);
-        String updatedUrl = uri.setUser(userName).toASCIIString();
-        gitConfig.setString("submodule", submoduleName, "url", updatedUrl);
-        String submodulePath = gitModules.getString("submodule", submoduleName, "path");
-        if (submodulePath != null && myPluginConfig.isUpdateSubmoduleOriginUrl()) {
-          File submoduleDir = new File(modulesDir, submodulePath);
-          if (submoduleDir.isDirectory() && new File(submoduleDir, Constants.CONFIG).isFile())
-            updateOriginUrl(submoduleDir, updatedUrl);
+      File modulesDir = new File(r.getDirectory(), Constants.MODULES);
+      for (String submoduleName : submodules) {
+        //The 'git submodule sync' command executed before resolves relative submodule urls
+        //from .gitmodules and writes them into .git/config. We should use resolved urls in
+        //order to add parent repository username to submodules with relative urls.
+        String url = gitConfig.getString("submodule", submoduleName, "url");
+        if (url == null) {
+          Loggers.VCS.info(".git/config doesn't contain an url for submodule '" + submoduleName + "', use url from .gitmodules");
+          url = gitModules.getString("submodule", submoduleName, "url");
+        }
+        Loggers.VCS.info("Update credentials for submodule with url " + url);
+        if (url == null || !isRequireAuth(url)) {
+          Loggers.VCS.info("Url " + url + " does not require authentication, skip updating credentials");
+          continue;
+        }
+        try {
+          URIish uri = new URIish(url);
+          String updatedUrl = uri.setUser(userName).toASCIIString();
+          gitConfig.setString("submodule", submoduleName, "url", updatedUrl);
+          String submodulePath = gitModules.getString("submodule", submoduleName, "path");
+          if (submodulePath != null && myPluginConfig.isUpdateSubmoduleOriginUrl()) {
+            File submoduleDir = new File(modulesDir, submodulePath);
+            if (submoduleDir.isDirectory() && new File(submoduleDir, Constants.CONFIG).isFile())
+              updateOriginUrl(submoduleDir, updatedUrl);
+          }
+          Loggers.VCS.debug("Submodule url " + url + " changed to " + updatedUrl);
+        } catch (URISyntaxException e) {
+          Loggers.VCS.warn("Error while parsing an url " + url + ", skip updating submodule credentials", e);
+        } catch (Exception e) {
+          Loggers.VCS.warn("Error while updating the '" + submoduleName + "' submodule url", e);
         }
-        Loggers.VCS.debug("Submodule url " + url + " changed to " + updatedUrl);
-      } catch (URISyntaxException e) {
-        Loggers.VCS.warn("Error while parsing an url " + url + ", skip updating submodule credentials", e);
-      } catch (Exception e) {
-        Loggers.VCS.warn("Error while updating the '" + submoduleName + "' submodule url", e);
       }
+      gitConfig.save();
+    } finally {
+      r.close();
     }
-    gitConfig.save();
   }
 
   private void updateOriginUrl(@NotNull File repoDir, @NotNull String url) throws IOException {
@@ -558,8 +569,8 @@ public class UpdaterImpl implements Updater {
       fetchAllBranches();
       if (!myFullBranchName.startsWith("refs/heads/")) {
         Ref remoteRef = getRef(myTargetDirectory, GitUtils.createRemoteRef(myFullBranchName));
-        if (!fetchRequired && remoteRef != null && myRevision.equals(remoteRef.getObjectId().name()) && hasRevision(myTargetDirectory, myRevision))
-          return;
+        if (fetchRequired || remoteRef == null || !myRevision.equals(remoteRef.getObjectId().name()) || !hasRevision(myTargetDirectory, myRevision))
+          fetchDefaultBranch();
       }
     } else {
       Ref remoteRef = getRef(myTargetDirectory, GitUtils.createRemoteRef(myFullBranchName));
@@ -634,7 +645,8 @@ public class UpdaterImpl implements Updater {
       .setAuthSettings(myRoot.getAuthSettings())
       .setUseNativeSsh(myPluginConfig.isUseNativeSSH())
       .setTimeout(timeout)
-      .setRefspec(refspec);
+      .setRefspec(refspec)
+      .setFetchTags(myPluginConfig.isFetchTags());
 
     if (silent)
       result.setQuite(true);
@@ -735,20 +747,27 @@ public class UpdaterImpl implements Updater {
     myLogger.message("The .git directory is missing in '" + myTargetDirectory + "'. Running 'git init'...");
     myGitFactory.create(myTargetDirectory).init().call();
     validateUrls();
-    myGitFactory.create(myRoot.getLocalRepositoryDir())
-      .addRemote()
-      .setName("origin")
-      .setUrl(myRoot.getRepositoryFetchURL().toString())
-      .call();
+    configureRemoteUrl(new File(myTargetDirectory, ".git"));
+
+    URIish fetchUrl = myRoot.getRepositoryFetchURL();
     URIish url = myRoot.getRepositoryPushURL();
     String pushUrl = url == null ? null : url.toString();
-    if (pushUrl != null && !pushUrl.equals(myRoot.getRepositoryFetchURL().toString())) {
+    if (pushUrl != null && !pushUrl.equals(fetchUrl.toString())) {
       myGitFactory.create(myTargetDirectory).setConfig().setPropertyName("remote.origin.pushurl").setValue(pushUrl).call();
     }
     setupNewRepository();
     configureSparseCheckout();
   }
 
+
+  void configureRemoteUrl(@NotNull File gitDir) throws VcsException {
+    RemoteRepositoryConfigurator cfg = new RemoteRepositoryConfigurator();
+    cfg.setGitDir(gitDir);
+    cfg.setExcludeUsernameFromHttpUrls(myPluginConfig.isExcludeUsernameFromHttpUrl() && !myPluginConfig.getGitVersion().isLessThan(UpdaterImpl.CREDENTIALS_SECTION_VERSION));
+    cfg.configure(myRoot);
+  }
+
+
   private void configureSparseCheckout() throws VcsException {
     if (myCheckoutMode == CheckoutMode.SPARSE_CHECKOUT) {
       setupSparseCheckout();
@@ -808,9 +827,7 @@ public class UpdaterImpl implements Updater {
     }
     final Refs remoteRefs;
     try {
-      remoteRefs = new Refs(git.lsRemote().setAuthSettings(myRoot.getAuthSettings())
-                              .setUseNativeSsh(myPluginConfig.isUseNativeSSH())
-                              .call());
+      remoteRefs = getRemoteRefs(workingDir);
     } catch (VcsException e) {
       if (CommandUtil.isCanceledError(e))
         throw e;
@@ -833,6 +850,19 @@ public class UpdaterImpl implements Updater {
     return outdatedRefsRemoved;
   }
 
+
+  @NotNull
+  private Refs getRemoteRefs(@NotNull File workingDir) throws VcsException {
+    if (myRemoteRefs != null)
+      return myRemoteRefs;
+    GitFacade git = myGitFactory.create(workingDir);
+    myRemoteRefs = new Refs(git.lsRemote().setAuthSettings(myRoot.getAuthSettings())
+      .setUseNativeSsh(myPluginConfig.isUseNativeSSH())
+      .call());
+    return myRemoteRefs;
+  }
+
+
   private boolean isRemoteTrackingBranch(@NotNull Ref localRef) {
     return localRef.getName().startsWith("refs/remotes/origin");
   }
@@ -847,6 +877,8 @@ public class UpdaterImpl implements Updater {
 
 
   private void configureLFS(@NotNull BaseCommand command) {
+    if (!myPluginConfig.isProvideCredHelper())
+      return;
     Trinity<String, String, String> lfsAuth = getLfsAuth();
     if (lfsAuth == null)
       return;
@@ -870,12 +902,14 @@ public class UpdaterImpl implements Updater {
       for (Map.Entry<String, String> e : config.getEnv().entrySet()) {
         command.setEnv(e.getKey(), e.getValue());
       }
-      command.addPostAction(new Runnable() {
-        @Override
-        public void run() {
-          FileUtil.delete(credHelper);
-        }
-      });
+      if (myPluginConfig.isCleanCredHelperScript()) {
+        command.addPostAction(new Runnable() {
+          @Override
+          public void run() {
+            FileUtil.delete(credHelper);
+          }
+        });
+      }
     } catch (Exception e) {
       if (credentialsHelper != null)
         FileUtil.delete(credentialsHelper);
@@ -910,4 +944,36 @@ public class UpdaterImpl implements Updater {
   private interface VcsCommand {
     void call() throws VcsException;
   }
+
+
+  /**
+   * Removes .idx files which don't have a corresponding .pack file
+   * @param ditGitDir git dir
+   */
+  void removeOrphanedIdxFiles(@NotNull File ditGitDir) {
+    if ("false".equals(myBuild.getSharedConfigParameters().get("teamcity.git.removeOrphanedIdxFiles"))) {
+      //looks like this logic is always needed, if no problems will be reported we can drop the option
+      return;
+    }
+    File packDir = new File(new File(ditGitDir, "objects"), "pack");
+    File[] files = packDir.listFiles();
+    if (files == null || files.length == 0)
+      return;
+
+    Set<String> packs = new HashSet<String>();
+    for (File f : files) {
+      String name = f.getName();
+      if (name.endsWith(".pack")) {
+        packs.add(name.substring(0, name.length() - 5));
+      }
+    }
+
+    for (File f : files) {
+      String name = f.getName();
+      if (name.endsWith(".idx")) {
+        if (!packs.contains(name.substring(0, name.length() - 4)))
+          FileUtil.delete(f);
+      }
+    }
+  }
 }
index 6563444af7246d894c49519d461afa18a8efb9bf..5e3674a3771ea9c16e7189e3f79a36da6bde945b 100644 (file)
@@ -117,7 +117,7 @@ public class UpdaterWithAlternates extends UpdaterWithMirror {
   }
 
   private void copyPackedRefs(@NotNull File targetDotGit) throws VcsException, IOException {
-    myGitFactory.create(myRoot.getRepositoryDir()).packRefs().call();
+    //packed-refs were prepared during mirror update
     FileUtil.copy(new File(myRoot.getRepositoryDir(), "packed-refs"), new File(targetDotGit, "packed-refs"));
   }
 }
index 0b6ff62c470b850274e61332904a85fbf2a4bdea..137f5ec36d77b788239f20999fc57961e8c6c8f7 100644 (file)
@@ -27,6 +27,7 @@ import jetbrains.buildServer.vcs.VcsException;
 import jetbrains.buildServer.vcs.VcsRoot;
 import org.apache.log4j.Logger;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryBuilder;
 import org.eclipse.jgit.transport.URIish;
 import org.jetbrains.annotations.NotNull;
@@ -34,9 +35,7 @@ import org.jetbrains.annotations.NotNull;
 import java.io.File;
 import java.io.IOException;
 import java.net.URISyntaxException;
-import java.util.HashSet;
 import java.util.Map;
-import java.util.Set;
 
 /**
  * @author dmitry.neverov
@@ -66,7 +65,15 @@ public class UpdaterWithMirror extends UpdaterImpl {
   }
 
   private void updateLocalMirror() throws VcsException {
-    updateLocalMirror(true);
+    String message = "Update git mirror (" + myRoot.getRepositoryDir() + ")";
+    myLogger.activityStarted(message, GitBuildProgressLogger.GIT_PROGRESS_ACTIVITY);
+    try {
+      updateLocalMirror(true);
+      //prepare refs for copying into working dir repository
+      myGitFactory.create(myRoot.getRepositoryDir()).packRefs().call();
+    } finally {
+      myLogger.activityFinished(message, GitBuildProgressLogger.GIT_PROGRESS_ACTIVITY);
+    }
   }
 
   private void updateLocalMirror(boolean repeatFetchAttempt) throws VcsException {
@@ -74,17 +81,21 @@ public class UpdaterWithMirror extends UpdaterImpl {
     String mirrorDescription = "local mirror of root " + myRoot.getName() + " at " + bareRepositoryDir;
     LOG.info("Update " + mirrorDescription);
     boolean fetchRequired = true;
-    if (!isValidGitRepo(bareRepositoryDir))
+    if (isValidGitRepo(bareRepositoryDir)) {
+      removeOrphanedIdxFiles(bareRepositoryDir);
+    } else {
       FileUtil.delete(bareRepositoryDir);
+    }
     boolean newMirror = false;
     if (!bareRepositoryDir.exists()) {
       LOG.info("Init " + mirrorDescription);
       bareRepositoryDir.mkdirs();
       GitFacade git = myGitFactory.create(bareRepositoryDir);
       git.init().setBare(true).call();
-      git.addRemote().setName("origin").setUrl(myRoot.getRepositoryFetchURL().toString()).call();
+      configureRemoteUrl(bareRepositoryDir);
       newMirror = true;
     } else {
+      configureRemoteUrl(bareRepositoryDir);
       boolean outdatedTagsFound = removeOutdatedRefs(bareRepositoryDir);
       if (!outdatedTagsFound) {
         LOG.debug("Try to find revision " + myRevision + " in " + mirrorDescription);
@@ -104,7 +115,6 @@ public class UpdaterWithMirror extends UpdaterImpl {
       GitFacade git = myGitFactory.create(bareRepositoryDir);
       git.gc().call();
       git.repack().call();
-      removeOrphanedIdxFiles(bareRepositoryDir);
     }
     if (myPluginConfig.isFetchAllHeads()) {
       String msg = getForcedHeadsFetchMessage();
@@ -126,29 +136,6 @@ public class UpdaterWithMirror extends UpdaterImpl {
   }
 
 
-  private void removeOrphanedIdxFiles(@NotNull File dotGitDir) {
-    File packDir = new File(new File(dotGitDir, "objects"), "pack");
-    File[] files = packDir.listFiles();
-    if (files == null)
-      return;
-
-    Set<String> packs = new HashSet<String>();
-    for (File f : files) {
-      String name = f.getName();
-      if (name.endsWith(".pack"))
-        packs.add(name.substring(0, name.length() - 5));
-    }
-
-    for (File f : files) {
-      String name = f.getName();
-      if (name.endsWith(".idx")) {
-        if (!packs.contains(name.substring(0, name.length() - 4)))
-          FileUtil.delete(f);
-      }
-    }
-  }
-
-
   private void fetchMirror(boolean repeatFetchAttempt,
                            @NotNull File repositoryDir,
                            @NotNull String refspec,
@@ -162,7 +149,7 @@ public class UpdaterWithMirror extends UpdaterImpl {
       if (cleanDir(repositoryDir)) {
         GitFacade git = myGitFactory.create(repositoryDir);
         git.init().setBare(true).call();
-        git.addRemote().setName("origin").setUrl(myRoot.getRepositoryFetchURL().toString()).call();
+        configureRemoteUrl(repositoryDir);
         fetch(repositoryDir, refspec, shallowClone);
       } else {
         LOG.info("Failed to delete repository " + repositoryDir + " after failed checkout, clone repository in another directory");
@@ -235,8 +222,26 @@ public class UpdaterWithMirror extends UpdaterImpl {
     }
   }
 
+
+  @NotNull
+  private String readRemoteUrl() throws VcsException {
+    Repository repository = null;
+    try {
+      repository = new RepositoryBuilder().setWorkTree(myTargetDirectory).build();
+      return repository.getConfig().getString("remote", "origin", "url");
+    } catch (IOException e) {
+      throw new VcsException("Error while reading remote repository url", e);
+    } finally {
+      if (repository != null)
+        repository.close();
+    }
+  }
+
+
   private void setUseLocalMirror() throws VcsException {
-    String remoteUrl = myRoot.getRepositoryFetchURL().toString();
+    //read remote url from config instead of VCS root, they can be different
+    //e.g. due to username exclusion from http(s) urls
+    String remoteUrl = readRemoteUrl();
     String localMirrorUrl = getLocalMirrorUrl();
     GitFacade git = myGitFactory.create(myTargetDirectory);
     git.setConfig()
index 0c226875b2dcf1694beeb63b6faf006d98b29d83..7687dd48b2527c889049c0650d55c746c47eb138 100644 (file)
@@ -43,6 +43,9 @@ public interface FetchCommand extends BaseCommand {
   @NotNull
   FetchCommand setDepth(int depth);
 
+  @NotNull
+  FetchCommand setFetchTags(boolean fetchTags);
+
   void call() throws VcsException;
 
 }
index fed8aabb0be234e949a0cbd704fecaad267df2c9..e2ad01aafcf96f5e47370a6606e2dc908a0db3a6 100644 (file)
@@ -21,6 +21,7 @@ import jetbrains.buildServer.agent.ClasspathUtil;
 import jetbrains.buildServer.buildTriggers.vcs.git.AuthSettings;
 import jetbrains.buildServer.buildTriggers.vcs.git.agent.CredentialsHelper;
 import jetbrains.buildServer.util.FileUtil;
+import jetbrains.buildServer.util.StringUtil;
 import org.jetbrains.annotations.NotNull;
 
 import java.io.*;
@@ -54,10 +55,19 @@ public abstract class ScriptGen {
       if (templateStream == null)
         throw new IOException("Cannot read script template " + template);
       String script = StreamUtil.readText(templateStream);
+      String javaPath = "java";
+      String javaHome = System.getProperty("java.home");
+      if (StringUtil.isNotEmpty(javaHome)) {
+        javaPath = "\"" + javaHome + File.separatorChar + "bin" + File.separatorChar + "java\"";
+      }
+      script = script.replace("{JAVA}", javaPath);
       script = script.replace("{CREDENTIALS_SCRIPT}", result.getCanonicalPath());
       script = script.replace("{CREDENTIALS_CLASSPATH}", ClasspathUtil.composeClasspath(new Class[]{CredentialsHelper.class}, null, null));
       script = script.replace("{CREDENTIALS_CLASS}", CredentialsHelper.class.getName());
-      out.print(script);
+      String[] lines = script.split("(\r\n|\r|\n)");
+      for (String line : lines) {
+        out.println(line);
+      }
       if (!result.setExecutable(true))
         throw new IOException("Cannot make credentialsHelper script executable");
     } catch (IOException e) {
index f6013cd307574ce3453da3da4feca0a10f95d197..530f8c923e1eb1f9155e0ade7a6fb8206656eb82 100644 (file)
@@ -109,11 +109,11 @@ public class CommandUtil {
         String inDir = workingDir != null ? "[" + workingDir.getAbsolutePath() + "]" : "";
         String msg = inDir + ": " + cmdStr;
         Loggers.VCS.info(msg);
-        cli.logStart(msg);
+        cli.logStart(cmdStr);
         ByteArrayOutputStream stdoutBuffer = new ByteArrayOutputStream();
         ByteArrayOutputStream stderrBuffer = cli.createStderrBuffer();
         ExecResult res = SimpleCommandLineProcessRunner.runCommandSecure(cli, cli.getCommandLineString(), null, new ProcessTimeoutCallback(timeoutSeconds), stdoutBuffer, stderrBuffer);
-        cli.logFinish(msg);
+        cli.logFinish(cmdStr);
         CommandUtil.checkCommandFailed(cmdStr, res, errorsLogLevel);
         String out = res.getStdout().trim();
         Loggers.VCS.debug(out);
index 99069a7c07d0b39fa3db3f7417ee4390ae4ef261..931ff0331ed0a800400f4ef39252bc7a35f72488 100644 (file)
@@ -38,6 +38,7 @@ public class FetchCommandImpl extends BaseCommandImpl implements FetchCommand {
   private boolean myShowProgress;
   private AuthSettings myAuthSettings;
   private Integer myDepth;
+  private boolean myFetchTags = true;
 
   public FetchCommandImpl(@NotNull GitCommandLine cmd) {
     super(cmd);
@@ -85,6 +86,13 @@ public class FetchCommandImpl extends BaseCommandImpl implements FetchCommand {
     return this;
   }
 
+  @NotNull
+  public FetchCommand setFetchTags(boolean fetchTags) {
+    myFetchTags = fetchTags;
+    return this;
+  }
+
+
   public void call() throws VcsException {
     GitCommandLine cmd = getCmd();
     cmd.addParameter("fetch");
@@ -94,6 +102,8 @@ public class FetchCommandImpl extends BaseCommandImpl implements FetchCommand {
       cmd.addParameter("--progress");
     if (myDepth != null)
       cmd.addParameter("--depth=" + myDepth);
+    if (!myFetchTags)
+      cmd.addParameter("--no-tags");
     cmd.addParameter("origin");
     cmd.addParameter(myRefspec);
     cmd.setHasProgress(true);
index e6d00fc4c5b6c805df54bddf1adad39d343d987c..01ee3ebd2df633c3c48a5343f42ad37c2342fc4b 100644 (file)
@@ -28,6 +28,6 @@ public class GitProgressListener implements LineAwareByteArrayOutputStream.LineL
   }
 
   public void newLineDetected(@NotNull String line) {
-    myLogger.message(line);
+    myLogger.progressMessage(line);
   }
 }
index c50ecc2ebab2bdc68d930ced4f42a7f4a006ce71..8c134a4b69419717e356cb321eb5dbd77f7c4469 100644 (file)
@@ -10,7 +10,7 @@
     <orderEntry type="library" name="TeamCity Open API common" level="project" />
     <orderEntry type="library" name="TeamCity Third-Party" level="project" />
     <orderEntry type="library" name="jgit" level="project" />
-    <orderEntry type="library" name="httpclient-4.3.4" level="project" />
+    <orderEntry type="library" name="httpclient" level="project" />
     <orderEntry type="library" name="slf4j-api-1.7.5" level="project" />
   </component>
 </module>
\ No newline at end of file
index 1c9a7ca989bdc7ada5d581e842729eb1398d0705..10d10dc811b98a7b6bb30b1b3536431a5151e426 100644 (file)
@@ -66,9 +66,14 @@ public class GitVcsRoot {
     myUsernameStyle = readUserNameStyle();
     mySubmodulePolicy = readSubmodulesPolicy();
     myAuthSettings = new AuthSettings(this);
-    myRepositoryFetchURL = myAuthSettings.createAuthURI(getProperty(Constants.FETCH_URL));
-    myRepositoryFetchURLNoFixErrors = myAuthSettings.createAuthURI(getProperty(Constants.FETCH_URL), false);
+    String rawFetchUrl = getProperty(Constants.FETCH_URL);
+    if (rawFetchUrl.contains("\n") || rawFetchUrl.contains("\r"))
+      throw new VcsException("Newline in fetch url '" + rawFetchUrl + "'");
+    myRepositoryFetchURL = myAuthSettings.createAuthURI(rawFetchUrl);
+    myRepositoryFetchURLNoFixErrors = myAuthSettings.createAuthURI(rawFetchUrl, false);
     String pushUrl = getProperty(Constants.PUSH_URL);
+    if (pushUrl != null && (pushUrl.contains("\n") || pushUrl.contains("\r")))
+      throw new VcsException("Newline in push url '" + pushUrl + "'");
     myRepositoryPushURL = StringUtil.isEmpty(pushUrl) ? myRepositoryFetchURL : myAuthSettings.createAuthURI(pushUrl);
     myRepositoryPushURLNoFixErrors = StringUtil.isEmpty(pushUrl) ? myRepositoryFetchURLNoFixErrors : myAuthSettings.createAuthURI(pushUrl, false);
     myUsernameForTags = getProperty(Constants.USERNAME_FOR_TAGS);
index d9c3eaf248b85bc0373c2b360d06bd15c46fd94c..32761f63fc9ca1622fe28e5a4cd40d5992e221db 100644 (file)
@@ -17,6 +17,7 @@
 package jetbrains.buildServer.buildTriggers.vcs.git;
 
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 import java.io.File;
 import java.util.Map;
@@ -31,7 +32,7 @@ public interface MirrorManager {
    * @return parent dir of local repositories
    */
   @NotNull
-  public File getBaseMirrorsDir();
+  File getBaseMirrorsDir();
 
   /**
    * Get default directory for remote repository with specified url
@@ -39,17 +40,27 @@ public interface MirrorManager {
    * @return see above
    */
   @NotNull
-  public File getMirrorDir(@NotNull String repositoryUrl);
+  File getMirrorDir(@NotNull String repositoryUrl);
 
   /**
    * Mark dir as invalid, urls mapped to this dir will get another mapping
    * on subsequent call to getMirrorDir()
    * @param dir dir of interest
    */
-  public void invalidate(@NotNull File dir);
+  void invalidate(@NotNull File dir);
 
 
-  public Map<String, File> getMappings();
+  @NotNull
+  Map<String, File> getMappings();
+
+  long getLastUsedTime(@NotNull final File dir);
 
-  public long getLastUsedTime(@NotNull final File dir);
+  /**
+   * Returns url for the given clone directory name inside the baseMirrorsDir
+   * or null if mapping from the url is not found
+   * @param cloneDirName clone directory name of interest
+   * @return see above
+   */
+  @Nullable
+  String getUrl(@NotNull String cloneDirName);
 }
index a29305022c2792c840baabc95d3700413320c0e4..dd4b71898afe0fe02801e67f3226d4719db761b7 100644 (file)
@@ -81,6 +81,7 @@ public class MirrorManagerImpl implements MirrorManager {
   }
 
 
+  @NotNull
   public Map<String, File> getMappings() {
     Map<String, String> mirrorMapSnapshot;
     synchronized (myLock) {
@@ -95,6 +96,19 @@ public class MirrorManagerImpl implements MirrorManager {
     return result;
   }
 
+  @Nullable
+  @Override
+  public String getUrl(@NotNull String cloneDirName) {
+    Map<String, String> mirrorMapSnapshot;
+    synchronized (myLock) {
+      mirrorMapSnapshot = new HashMap<String, String>(myMirrorMap);
+    }
+    for (Map.Entry<String, String> e : mirrorMapSnapshot.entrySet()) {
+      if (cloneDirName.equals(e.getValue()))
+        return e.getKey();
+    }
+    return null;
+  }
 
   public long getLastUsedTime(@NotNull final File dir) {
     File timestamp = new File(dir, "timestamp");
index 185f3605890a1c72f77942305932334668628b0f..69c5c1b38fa36e97c3cad7c868e9bc74b180bb25 100644 (file)
@@ -1,6 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <dsl-extension kind="vcs" type="jetbrains.git" generateDslJar="true">
-  <class name="GitVcsRoot"/>
+  <class name="GitVcsRoot">
+    <description>Git [VCS root](https://confluence.jetbrains.com/display/TCDL/Git)</description>
+  </class>
   <params>
     <param name="url">
       <description>Repository url</description>
     </param>
     <param name="usernameStyle" dslName="userNameStyle" type="UserNameStyle">
       <description>
-        Defines a way how TeamCity binds VCS changes to the users.
+        Defines how TeamCity retrieves [VCS username](https://confluence.jetbrains.com/display/TCDL/Managing+Users+and+User+Groups#ManagingUsersandUserGroups-vcsUsername)
+        from git commit.
+        @see UserNameStyle
       </description>
     </param>
     <param name="submoduleCheckout" type="CheckoutSubmodules" dslName="checkoutSubmodules">
       <description>
         Whether VCS root should include changes in submodules and check their sources for build.
-        By default true.
+        By default submodules are checked out.
+        @see CheckoutSubmodules
       </description>
     </param>
     <param name="userForTags">
       </description>
     </param>
     <param name="agentCleanPolicy" type="AgentCleanPolicy">
-      <description>Specifies when the "git clean" command should be executed in case of agent-side checkout</description>
+      <description>
+        Specifies when the "git clean" command should be executed in case of agent-side checkout
+        @see AgentCleanPolicy
+      </description>
     </param>
     <param name="agentCleanFilesPolicy" type="AgentCleanFilesPolicy">
       <description>
         Specifies which files should be removed when "git clean" command is executed during agent-side checkout.
+        @see AgentCleanFilesPolicy
       </description>
     </param>
     <param name="useAlternates" dslName="useMirrors" type="boolean" trueValue="true" falseValue="">
       <description>
         VCS Root authentication method
       </description>
-      <option name="anonymous" value="ANONYMOUS"/>
+      <option name="anonymous" value="ANONYMOUS">
+        <description>Anonymous repository access</description>
+      </option>
       <option name="password" value="PASSWORD">
-        <param name="username" dslName="userName"/>
-        <param name="secure:password" dslName="password"/>
+        <description>Password authentication</description>
+        <param name="username" dslName="userName">
+          <description>Username to use, overwrites the username in the url</description>
+        </param>
+        <param name="secure:password" dslName="password">
+          <description>Password to use</description>
+        </param>
       </option>
       <option name="uploadedKey" value="TEAMCITY_SSH_KEY">
-        <param name="username" dslName="userName"/>
-        <param name="teamcitySshKey" dslName="uploadedKey"/>
-        <param name="secure:passphrase" dslName="passphrase"/>
+        <description>
+          Uploaded [SSH key](https://confluence.jetbrains.com/display/TCDL/SSH+Keys+Management) with the specified name.
+        </description>
+        <param name="username" dslName="userName">
+          <description>Username to use, overwrites the username in the url</description>
+        </param>
+        <param name="teamcitySshKey" dslName="uploadedKey">
+          <description>Name of the uploaded [SSH key](https://confluence.jetbrains.com/display/TCDL/SSH+Keys+Management) to use</description>
+        </param>
+        <param name="secure:passphrase" dslName="passphrase">
+          <description>
+            Passphrase for the uploaded [SSH key](https://confluence.jetbrains.com/display/TCDL/SSH+Keys+Management).
+            Leave it empty if the key is not encrypted.
+          </description>
+        </param>
       </option>
       <option name="defaultPrivateKey" value="PRIVATE_KEY_DEFAULT">
-        <param name="username" dslName="userName"/>
+        <description>
+          Default SSH key found on the machine.
+          If you use an agent-side checkout, then this key should also be available on the build agent machines.
+          Often it is easier to use the uploaded SSH key.
+          @see uploadedKey
+        </description>
+        <param name="username" dslName="userName">
+          <description>Username to use, overwrites the username in the url</description>
+        </param>
       </option>
       <option name="customPrivateKey" value="PRIVATE_KEY_FILE">
-        <param name="username" dslName="userName"/>
-        <param name="privateKeyPath" dslName="customKeyPath"/>
-        <param name="secure:passphrase" dslName="passphrase"/>
+        <description>
+          SSH key on the specified path. Supported only for server-side checkout.
+          Switch to uploaded SSH key if you want to use an agent-side checkout.
+          @see uploadedKey
+        </description>
+        <param name="username" dslName="userName">
+          <description>Username to use, overwrites the username in the url</description>
+        </param>
+        <param name="privateKeyPath" dslName="customKeyPath">
+          <description>Path to the SSH key on TeamCity server machine</description>
+        </param>
+        <param name="secure:passphrase" dslName="passphrase">
+          <description>
+            Passphrase for the key. Leave it empty if the key is not encrypted.
+          </description>
+        </param>
       </option>
     </param>
   </params>
   <types>
     <enum name="UserNameStyle">
-      <option name="NAME"/>
-      <option name="USERID"/>
-      <option name="EMAIL"/>
-      <option name="FULL"/>
+      <description>
+        Defines how TeamCity retrieves [VCS username](https://confluence.jetbrains.com/display/TCDL/Managing+Users+and+User+Groups#ManagingUsersandUserGroups-vcsUsername)
+        from git commit.
+
+        When the git config contains the following
+
+        ```
+        [user]
+          name = Joe Coder
+          email = joe.coder@acme.com
+        ```
+
+        then the git username in commit is `Joe Coder &lt;joe.coder@acme.com>`.
+
+        Different options specify which part of the git commit username is used in TeamCity.
+      </description>
+      <option name="NAME">
+        <description>Use the name part, for full name `Joe Coder &lt;joe.coder@acme.com>` it will be `Joe Coder`</description>
+      </option>
+      <option name="USERID">
+        <description>Use part of the email before the @ sign, for full name `Joe Coder &lt;joe.coder@acme.com>` it will be `joe.coder`</description>
+      </option>
+      <option name="EMAIL">
+        <description>Use the email part, for full name `Joe Coder &lt;joe.coder@acme.com>` it will be `joe.coder@acme.com`</description>
+      </option>
+      <option name="FULL">
+        <description>Use full commit username, i.e. `Joe Coder &lt;joe.coder@acme.com>`</description>
+      </option>
     </enum>
     <enum name="CheckoutSubmodules">
-      <option name="SUBMODULES_CHECKOUT"/>
-      <option name="IGNORE"/>
+      <description>Submodules checkout mode</description>
+      <option name="SUBMODULES_CHECKOUT">
+        <description>Checkout submodules and show submodule changes in UI</description>
+      </option>
+      <option name="IGNORE">
+        <description>Don't checkout submodules and don't show changes from submodules in UI</description>
+      </option>
     </enum>
     <enum name="AgentCleanPolicy">
-      <option name="NEVER"/>
-      <option name="ALWAYS"/>
-      <option name="ON_BRANCH_CHANGE"/>
+      <description>Specifies when the "git clean" command should be executed in case of agent-side checkout</description>
+      <option name="NEVER">
+        <description>Don't run the "git clean" command</description>
+      </option>
+      <option name="ALWAYS">
+        <description>Run the "git clean" command before each build</description>
+      </option>
+      <option name="ON_BRANCH_CHANGE">
+        <description>
+          Run the "git clean" command if the branch in build is different comparing to the branch in the previous build on same agent
+        </description>
+      </option>
     </enum>
     <enum name="AgentCleanFilesPolicy">
-      <option name="IGNORED_ONLY"/>
-      <option name="NON_IGNORED_ONLY"/>
-      <option name="ALL_UNTRACKED"/>
+      <description>Specifies flags for the "git clean" command during agent-side checkout and defines which files will be removed.</description>
+      <option name="IGNORED_ONLY">
+        <description>Will run "git clean -dfX"</description>
+      </option>
+      <option name="NON_IGNORED_ONLY">
+        <description>Will run "git clean -df"</description>
+      </option>
+      <option name="ALL_UNTRACKED">
+        <description>Will run "git clean -dfx"</description>
+      </option>
     </enum>
   </types>
-</dsl-extension>
\ No newline at end of file
+</dsl-extension>
index b290f912797d635b049cde8f782cac1f0e8309b1..205575c5ebdd2b31b1515abbaa975adc9460c18d 100644 (file)
@@ -24,6 +24,6 @@
     <orderEntry type="module" module-name="git-common" />
     <orderEntry type="library" name="TeamCity Third-Party" level="project" />
     <orderEntry type="library" name="jgit" level="project" />
+    <orderEntry type="library" name="Tomcat" level="project" />
   </component>
-</module>
-
+</module>
\ No newline at end of file
index fcd93cba6a31a0393a23a5dd7dc52be7fa0440db..27bdc67948ad46c359326a166ac7df69cf8b87b5 100644 (file)
@@ -8,4 +8,8 @@
   <bean class="jetbrains.buildServer.buildTriggers.vcs.git.GitBranchSupport"/>
   <bean class="jetbrains.buildServer.buildTriggers.vcs.git.GitBuildParametersProvider"/>
   <bean class="jetbrains.buildServer.buildTriggers.vcs.git.GitSubmodulesUsageStatistics"/>
+  <bean class="jetbrains.buildServer.buildTriggers.vcs.git.health.GitNotFoundHealthReport"/>
+  <bean class="jetbrains.buildServer.buildTriggers.vcs.git.health.GitNotFoundHealthPage"/>
+  <bean class="jetbrains.buildServer.buildTriggers.vcs.git.health.GitGcErrorsHealthReport"/>
+  <bean class="jetbrains.buildServer.buildTriggers.vcs.git.health.GitGcErrorsHealthPage"/>
 </beans>
\ No newline at end of file
diff --git a/git-server-tc/src/jetbrains/buildServer/buildTriggers/vcs/git/health/GitGcErrorsHealthPage.java b/git-server-tc/src/jetbrains/buildServer/buildTriggers/vcs/git/health/GitGcErrorsHealthPage.java
new file mode 100644 (file)
index 0000000..283c526
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2000-2017 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.health;
+
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.util.Pair;
+import jetbrains.buildServer.buildTriggers.vcs.git.MirrorManager;
+import jetbrains.buildServer.serverSide.ServerPaths;
+import jetbrains.buildServer.serverSide.auth.Permission;
+import jetbrains.buildServer.serverSide.healthStatus.HealthStatusItem;
+import jetbrains.buildServer.util.FileUtil;
+import jetbrains.buildServer.web.openapi.PagePlaces;
+import jetbrains.buildServer.web.openapi.PluginDescriptor;
+import jetbrains.buildServer.web.openapi.healthStatus.HealthStatusItemPageExtension;
+import jetbrains.buildServer.web.util.SessionUser;
+import org.jetbrains.annotations.NotNull;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+public class GitGcErrorsHealthPage extends HealthStatusItemPageExtension {
+
+  private static final Logger LOG = Logger.getInstance(GitGcErrorsHealthPage.class.getName());
+  private final ServerPaths myServerPaths;
+  private final MirrorManager myMirrorManager;
+
+  public GitGcErrorsHealthPage(@NotNull PluginDescriptor pluginDescriptor,
+                               @NotNull PagePlaces pagePlaces,
+                               @NotNull ServerPaths serverPaths,
+                               @NotNull MirrorManager mirrorManager) {
+    super(GitGcErrorsHealthReport.REPORT_TYPE, pagePlaces);
+    myServerPaths = serverPaths;
+    myMirrorManager = mirrorManager;
+    setIncludeUrl(pluginDescriptor.getPluginResourcesPath("health/gitGcErrorsReport.jsp"));
+    setVisibleOutsideAdminArea(false);
+    register();
+  }
+
+  @Override
+  public boolean isAvailable(@NotNull HttpServletRequest request) {
+    if (!super.isAvailable(request))
+      return false;
+    if (!SessionUser.getUser(request).isPermissionGrantedGlobally(Permission.CHANGE_SERVER_SETTINGS)) return false;
+    HealthStatusItem item = getStatusItem(request);
+    Object path = item.getAdditionalData().get(GitGcErrorsHealthReport.ERRORS_KEY);
+    return path instanceof Map && !((Map) path).isEmpty();
+  }
+
+  @Override
+  public void fillModel(@NotNull final Map<String, Object> model, @NotNull final HttpServletRequest request) {
+    HealthStatusItem item = getStatusItem(request);
+    Object errors = item.getAdditionalData().get(GitGcErrorsHealthReport.ERRORS_KEY);
+    Map<String, Pair<String, String>> sortedErrors = new TreeMap<>();
+    if (errors instanceof Map) {
+      String baseDir;
+      try {
+        baseDir = new File(myServerPaths.getCachesDir(), "git").getCanonicalPath();
+      } catch (IOException e) {
+        baseDir = new File(myServerPaths.getCachesDir(), "git").getAbsolutePath();
+      }
+
+      //noinspection unchecked
+      Set<Map.Entry> entries = ((Map)errors).entrySet();
+      for (Map.Entry entry : entries) {
+        Object key = entry.getKey();
+        Object value = entry.getValue();
+        if (key instanceof File && value instanceof String) {
+          try {
+            String relativePath = FileUtil.getRelativePath(baseDir, ((File)key).getCanonicalPath(), File.separatorChar);
+            String url = myMirrorManager.getUrl(relativePath);
+            if (url != null) {
+              sortedErrors.put(url, Pair.create(relativePath, (String) value));
+            }
+          } catch (IOException e) {
+            LOG.warnAndDebugDetails("Error while preparing health report data", e);
+          }
+        }
+      }
+    }
+    model.put("errors", sortedErrors);
+  }
+}
diff --git a/git-server-tc/src/jetbrains/buildServer/buildTriggers/vcs/git/health/GitGcErrorsHealthReport.java b/git-server-tc/src/jetbrains/buildServer/buildTriggers/vcs/git/health/GitGcErrorsHealthReport.java
new file mode 100644 (file)
index 0000000..a00ddcb
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2000-2017 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.health;
+
+import jetbrains.buildServer.buildTriggers.vcs.git.GcErrors;
+import jetbrains.buildServer.serverSide.healthStatus.*;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class GitGcErrorsHealthReport extends HealthStatusReport {
+
+  private static final String PREFIX = "gitGcError";
+  static final String REPORT_TYPE = PREFIX + "HealthReport";
+  private static final ItemCategory CATEGORY = new ItemCategory(PREFIX + "HealthCategory", "Git garbage collection error", ItemSeverity.WARN);
+  static final String ERRORS_KEY = "errors";
+
+  private final GcErrors myGcErrors;
+
+  public GitGcErrorsHealthReport(@NotNull GcErrors gcErrors) {
+    myGcErrors = gcErrors;
+  }
+
+  @NotNull
+  @Override
+  public String getType() {
+    return REPORT_TYPE;
+  }
+
+  @NotNull
+  @Override
+  public String getDisplayName() {
+    return CATEGORY.getName();
+  }
+
+  @NotNull
+  @Override
+  public Collection<ItemCategory> getCategories() {
+    return Collections.singleton(CATEGORY);
+  }
+
+  @Override
+  public boolean canReportItemsFor(@NotNull HealthStatusScope scope) {
+    return scope.globalItems() && scope.isItemWithSeverityAccepted(ItemSeverity.WARN);
+  }
+
+  @Override
+  public void report(@NotNull HealthStatusScope scope, @NotNull HealthStatusItemConsumer resultConsumer) {
+    Map<File, String> errors = myGcErrors.getErrors();
+    if (!errors.isEmpty()) {
+      Map<String, Object> data = new HashMap<>();
+      data.put(ERRORS_KEY, errors);
+      resultConsumer.consumeGlobal(new HealthStatusItem(PREFIX + "HealthItemId", CATEGORY, ItemSeverity.WARN, data));
+    }
+  }
+}
diff --git a/git-server-tc/src/jetbrains/buildServer/buildTriggers/vcs/git/health/GitNotFoundHealthPage.java b/git-server-tc/src/jetbrains/buildServer/buildTriggers/vcs/git/health/GitNotFoundHealthPage.java
new file mode 100644 (file)
index 0000000..c945e9d
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2000-2017 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.health;
+
+import jetbrains.buildServer.serverSide.auth.Permission;
+import jetbrains.buildServer.serverSide.healthStatus.HealthStatusItem;
+import jetbrains.buildServer.vcs.VcsException;
+import jetbrains.buildServer.web.openapi.PagePlaces;
+import jetbrains.buildServer.web.openapi.PluginDescriptor;
+import jetbrains.buildServer.web.openapi.healthStatus.HealthStatusItemPageExtension;
+import jetbrains.buildServer.web.util.SessionUser;
+import org.jetbrains.annotations.NotNull;
+
+import javax.servlet.http.HttpServletRequest;
+
+public class GitNotFoundHealthPage extends HealthStatusItemPageExtension {
+
+  public GitNotFoundHealthPage(@NotNull PluginDescriptor pluginDescriptor,
+                               @NotNull PagePlaces pagePlaces) {
+    super(GitNotFoundHealthReport.REPORT_TYPE, pagePlaces);
+    setIncludeUrl(pluginDescriptor.getPluginResourcesPath("health/gitNotFoundReport.jsp"));
+    setVisibleOutsideAdminArea(false);
+    register();
+  }
+
+  @Override
+  public boolean isAvailable(@NotNull HttpServletRequest request) {
+    if (!super.isAvailable(request))
+      return false;
+    if (!SessionUser.getUser(request).isPermissionGrantedGlobally(Permission.CHANGE_SERVER_SETTINGS)) return false;
+    HealthStatusItem item = getStatusItem(request);
+    Object path = item.getAdditionalData().get(GitNotFoundHealthReport.PATH_KEY);
+    Object error = item.getAdditionalData().get(GitNotFoundHealthReport.ERROR_KEY);
+    return path instanceof String && error instanceof VcsException;
+  }
+}
diff --git a/git-server-tc/src/jetbrains/buildServer/buildTriggers/vcs/git/health/GitNotFoundHealthReport.java b/git-server-tc/src/jetbrains/buildServer/buildTriggers/vcs/git/health/GitNotFoundHealthReport.java
new file mode 100644 (file)
index 0000000..07b2ffb
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2000-2017 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.health;
+
+import jetbrains.buildServer.buildTriggers.vcs.git.Cleanup;
+import jetbrains.buildServer.serverSide.healthStatus.*;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class GitNotFoundHealthReport extends HealthStatusReport {
+
+  private static final String PREFIX = "gitNotFound";
+  static final String REPORT_TYPE = PREFIX + "HealthReport";
+  private static final ItemCategory CATEGORY = new ItemCategory(PREFIX + "HealthCategory", "Git executable not found", ItemSeverity.INFO);
+  static final String PATH_KEY = "path";
+  static final String ERROR_KEY = "error";
+
+  private final Cleanup myCleanup;
+
+
+  public GitNotFoundHealthReport(@NotNull Cleanup cleanup) {
+    myCleanup = cleanup;
+  }
+
+  @NotNull
+  @Override
+  public String getType() {
+    return REPORT_TYPE;
+  }
+
+  @NotNull
+  @Override
+  public String getDisplayName() {
+    return "Git executable not found";
+  }
+
+  @NotNull
+  @Override
+  public Collection<ItemCategory> getCategories() {
+    return Collections.singletonList(CATEGORY);
+  }
+
+  @Override
+  public boolean canReportItemsFor(@NotNull final HealthStatusScope scope) {
+    return scope.globalItems() && scope.isItemWithSeverityAccepted(ItemSeverity.WARN);
+  }
+
+  @Override
+  public void report(@NotNull HealthStatusScope scope, @NotNull HealthStatusItemConsumer resultConsumer) {
+    Cleanup.RunGitError error = myCleanup.getNativeGitError();
+    if (error != null) {
+      Map<String, Object> data = new HashMap<>();
+      data.put(PATH_KEY, error.getGitPath());
+      data.put(ERROR_KEY, error.getError());
+      resultConsumer.consumeGlobal(new HealthStatusItem(PREFIX + "HealthItemId", CATEGORY, ItemSeverity.INFO, data));
+    }
+  }
+}
index 4c55ed3ef9321d5df9b3ff44668e2cfd71525159..7fa560ad75f9c805b71b3a7c01f848cd5d709f9b 100644 (file)
@@ -22,7 +22,7 @@
     <orderEntry type="library" name="jgit" level="project" />
     <orderEntry type="library" name="quartz-1.6.0" level="project" />
     <orderEntry type="library" name="org.eclipse.egit.github.core-2.4.0-SNAPSHOT" level="project" />
-    <orderEntry type="library" name="httpclient-4.3.4" level="project" />
+    <orderEntry type="library" name="httpclient" level="project" />
     <orderEntry type="library" name="TeamCity patches" level="project" />
     <orderEntry type="library" name="slf4j-api-1.7.5" level="project" />
     <orderEntry type="library" name="JavaEWAH-0.7.9" level="project" />
diff --git a/git-server/resources/buildServerResources/health/gitGcErrorsReport.jsp b/git-server/resources/buildServerResources/health/gitGcErrorsReport.jsp
new file mode 100644 (file)
index 0000000..66fadba
--- /dev/null
@@ -0,0 +1,18 @@
+<%@ page import="jetbrains.buildServer.web.openapi.healthStatus.HealthStatusItemDisplayMode" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<c:set var="inplaceMode" value="<%=HealthStatusItemDisplayMode.IN_PLACE%>"/>
+<jsp:useBean id="showMode" type="jetbrains.buildServer.web.openapi.healthStatus.HealthStatusItemDisplayMode" scope="request"/>
+<c:set var="errorsBlockId" value="gitGcErrors_${showMode}"/>
+<jsp:useBean id="errors" type="java.util.Map<java.lang.String, java.lang.String>" scope="request"/>
+
+<div>
+  Errors while running git garbage collection (paths are relative to the '&lt;TeamCity data dir&gt;/system/caches/git' directory)
+  <c:if test="${showMode == inplaceMode}"><a href="javascript:;" onclick="$j('#${errorsBlockId}').toggle();">Show details &raquo;</a></c:if>
+</div>
+<div id="${errorsBlockId}" style="margin-left: 1em; display: ${showMode == inplaceMode ? 'none' : ''}">
+  <c:forEach var="error" items="${errors}">
+    <div>
+      <b><c:out value="${error.key}"/></b> (clone dir <c:out value="${error.value.first}"/>): <c:out value="${error.value.second}"/>
+    </div>
+  </c:forEach>
+</div>
diff --git a/git-server/resources/buildServerResources/health/gitNotFoundReport.jsp b/git-server/resources/buildServerResources/health/gitNotFoundReport.jsp
new file mode 100644 (file)
index 0000000..5825dff
--- /dev/null
@@ -0,0 +1,12 @@
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ taglib prefix="bs" tagdir="/WEB-INF/tags" %>
+<jsp:useBean id="healthStatusItem" type="jetbrains.buildServer.serverSide.healthStatus.HealthStatusItem" scope="request"/>
+<c:set var="path" value="${healthStatusItem.additionalData['path']}"/>
+<c:set var="error" value="${healthStatusItem.additionalData['error']}"/>
+<div>
+  Cannot run git garbage collection using git at path '<c:out value="${path}"/>': <c:out value="${error.message}"/>.
+  <br/>
+  Please install a git client on the TeamCity server machine and specify a path to it in the <b>teamcity.server.git.executable.path</b>
+  <bs:helpLink file="Configuring+TeamCity+Server+Startup+Properties" anchor="TeamCityinternalproperties">internal property</bs:helpLink>
+  <bs:helpLink file="Git" anchor="Git_gc"><bs:helpIcon/></bs:helpLink>.
+</div>
\ No newline at end of file
index 73048fa3e55a2a72e5aee3c554ea7a6329132321..a8b2cc12982961171d63b77baf8cceb8a4371758 100755 (executable)
@@ -28,7 +28,9 @@
   <bean id="hashCalculator" class="jetbrains.buildServer.buildTriggers.vcs.git.HashCalculatorImpl"/>
   <bean id="repositoryManager" class="jetbrains.buildServer.buildTriggers.vcs.git.RepositoryManagerImpl"/>
   <bean id="mapFullPath" class="jetbrains.buildServer.buildTriggers.vcs.git.GitMapFullPath"/>
-  <bean id="cleaner" class="jetbrains.buildServer.buildTriggers.vcs.git.CleanupRunner"/>
+  <bean class="jetbrains.buildServer.buildTriggers.vcs.git.GcErrors"/>
+  <bean id="cleaner" class="jetbrains.buildServer.buildTriggers.vcs.git.Cleanup"/>
+  <bean id="cleanerRunner" class="jetbrains.buildServer.buildTriggers.vcs.git.CleanupRunner"/>
   <bean id="fetcherProperties" class="jetbrains.buildServer.buildTriggers.vcs.git.FetcherProperties"/>
   <bean id="mergeSupport" class="jetbrains.buildServer.buildTriggers.vcs.git.GitMergeSupport"/>
   <bean class="jetbrains.buildServer.buildTriggers.vcs.git.commitInfo.GitCommitsInfoBuilder"/>
index 0892e20d70226bf25e00356edf5ffd74a55525f7..c2bfc1f55d480ebd5d8c47a25d67a6d4ebe0d0d4 100644 (file)
@@ -18,12 +18,18 @@ package jetbrains.buildServer.buildTriggers.vcs.git;
 
 import com.intellij.execution.configurations.GeneralCommandLine;
 import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.util.Pair;
 import jetbrains.buildServer.ExecResult;
 import jetbrains.buildServer.SimpleCommandLineProcessRunner;
 import jetbrains.buildServer.log.Loggers;
 import jetbrains.buildServer.util.Dates;
 import jetbrains.buildServer.util.FileUtil;
+import jetbrains.buildServer.util.StringUtil;
 import jetbrains.buildServer.vcs.VcsException;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.internal.storage.file.PackFile;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryBuilder;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
@@ -32,32 +38,50 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.concurrent.locks.Lock;
+import java.util.regex.Pattern;
 
 public class Cleanup {
 
-  private static Logger LOG = Loggers.CLEANUP;
+  private static final Logger LOG = Loggers.CLEANUP;
+  private static final Pattern PATTERN_LOOSE_OBJECT = Pattern.compile("[0-9a-fA-F]{38}");
+  private static final Semaphore ourSemaphore = new Semaphore(1);
 
   private final RepositoryManager myRepositoryManager;
   private final ServerPluginConfig myConfig;
+  private final GcErrors myGcErrors;
+  private final AtomicReference<RunGitError> myNativeGitError = new AtomicReference<>();
 
   public Cleanup(@NotNull final ServerPluginConfig config,
-                 @NotNull final RepositoryManager repositoryManager) {
+                 @NotNull final RepositoryManager repositoryManager,
+                 @NotNull final GcErrors gcErrors) {
     myConfig = config;
     myRepositoryManager = repositoryManager;
+    myGcErrors = gcErrors;
   }
 
   public void run() {
-    LOG.info("Git cleanup started");
-    removeUnusedRepositories();
-    cleanupMonitoringData();
-    if (myConfig.isRunJGitGC()) {
-      runJGitGC();
-    } else if (myConfig.isRunNativeGC()) {
-      runNativeGC();
+    if (!ourSemaphore.tryAcquire()) {
+      LOG.info("Skip git cleanup: another git cleanup process is running");
+      return;
+    }
+
+    try {
+      LOG.info("Git cleanup started");
+      removeUnusedRepositories();
+      cleanupMonitoringData();
+      if (myConfig.isRunNativeGC()) {
+        runNativeGC();
+      } else if (myConfig.isRunJGitGC()) {
+        runJGitGC();
+      }
+      LOG.info("Git cleanup finished");
+    } finally {
+      ourSemaphore.release();
     }
-    LOG.info("Git cleanup finished");
   }
 
   private void removeUnusedRepositories() {
@@ -74,6 +98,7 @@ public class Cleanup {
         rmLock.unlock();
       }
       if (deleted) {
+        myGcErrors.clearError(dir);
         LOG.debug("Remove unused git repository dir " + dir.getAbsolutePath() + " finished");
       } else {
         LOG.error("Cannot delete unused git repository dir " + dir.getAbsolutePath());
@@ -92,10 +117,6 @@ public class Cleanup {
     return new ArrayList<File>(FileUtil.getSubDirectories(myRepositoryManager.getBaseMirrorsDir()));
   }
 
-  private long minutes2Milliseconds(int quotaInMinutes) {
-    return quotaInMinutes * 60 * 1000L;
-  }
-
   private void cleanupMonitoringData() {
     LOG.debug("Start cleaning git monitoring data");
     for (File repository : getAllRepositoryDirs()) {
@@ -120,19 +141,42 @@ public class Cleanup {
   }
 
   private void runNativeGC() {
+    final long startNanos = System.nanoTime();
+    final long gcTimeQuotaNanos = TimeUnit.MINUTES.toNanos(myConfig.getNativeGCQuotaMinutes());
+    List<File> allDirs = getAllRepositoryDirs();
+    myGcErrors.retainErrors(allDirs);
+    if (allDirs.isEmpty()) {
+      LOG.debug("No repositories found");
+      //reset error, no reason to show it if there is no repositories
+      myNativeGitError.set(null);
+      return;
+    }
     if (!isNativeGitInstalled()) {
       LOG.info("Cannot find native git, skip running git gc");
       return;
     }
-    final long startNanos = System.nanoTime();
-    final long gcTimeQuotaNanos = TimeUnit.MINUTES.toNanos(myConfig.getNativeGCQuotaMinutes());
-    LOG.info("Git garbage collection started");
-    List<File> allDirs = getAllRepositoryDirs();
+    Long freeDiskSpace = FileUtil.getFreeSpace(myRepositoryManager.getBaseMirrorsDir());
+    LOG.info("Use git at path '" + myConfig.getPathToGit() + "'");
     Collections.shuffle(allDirs);
     int runGCCounter = 0;
+    LOG.info("Git garbage collection started");
+    boolean runInPlace = myConfig.runInPlaceGc();
     for (File gitDir : allDirs) {
-      synchronized (myRepositoryManager.getWriteLock(gitDir)) {
-        runNativeGC(gitDir);
+      String url = myRepositoryManager.getUrl(gitDir.getName());
+      if (url != null) {
+        LOG.info("[" + gitDir.getName() + "] repository url: '" + url + "'");
+      }
+      if (enoughDiskSpaceForGC(gitDir, freeDiskSpace)) {
+        if (runInPlace) {
+          synchronized (myRepositoryManager.getWriteLock(gitDir)) {
+            runNativeGC(gitDir);
+          }
+        } else {
+          runGcInCopy(gitDir);
+        }
+      } else {
+        myGcErrors.registerError(gitDir, "Not enough disk space to run git gc");
+        LOG.warn("[" + gitDir.getName() + "] not enough disk space to run git gc (" + String.valueOf(freeDiskSpace) + " " + pluralize("byte", freeDiskSpace) + ")");
       }
       runGCCounter++;
       final long repositoryFinishNanos = System.nanoTime();
@@ -148,6 +192,251 @@ public class Cleanup {
     LOG.info("Git garbage collection finished, it took " + TimeUnit.NANOSECONDS.toMillis(finishNanos - startNanos) + "ms");
   }
 
+
+  private void runGcInCopy(@NotNull File originalRepo) {
+    Lock rmLock = myRepositoryManager.getRmLock(originalRepo).readLock();
+    rmLock.lock();
+    File gcRepo;
+    try {
+      if (!isGcNeeded(originalRepo)) {
+        LOG.info("[" + originalRepo.getName() + "] no git gc is needed");
+        myGcErrors.clearError(originalRepo);
+        return;
+      }
+
+      try {
+        gcRepo = setupGcRepo(originalRepo);
+      } catch (Exception e) {
+        myGcErrors.registerError(originalRepo, "Failed to create temporary repository for garbage collection", e);
+        LOG.warnAndDebugDetails("Failed to create temporary repository for garbage collection, original repository: " + originalRepo.getAbsolutePath(), e);
+        return;
+      }
+
+      LOG.info("[" + originalRepo.getName() + "] run git gc in dedicated dir [" + gcRepo.getName() + "]");
+
+      try {
+        repack(gcRepo);
+        packRefs(gcRepo);
+      } catch (Exception e) {
+        myGcErrors.registerError(originalRepo, "Error while running garbage collection", e);
+        LOG.warnAndDebugDetails("Error while running garbage collection in " + originalRepo.getAbsolutePath(), e);
+        FileUtil.delete(gcRepo);
+        return;
+      }
+    } finally {
+      rmLock.unlock();
+    }
+
+    //remove alternates pointing to the original repo before swapping repositories
+    FileUtil.delete(new File(gcRepo, "objects/info/alternates"));
+
+    long swapStart = System.currentTimeMillis();
+    File oldDir;
+    try {
+      oldDir = createTempDir(originalRepo.getParentFile(), originalRepo.getName() + ".old");
+      FileUtil.delete(oldDir);
+    } catch (Exception e) {
+      myGcErrors.registerError(originalRepo, "Error while creating temporary directory", e);
+      LOG.warnAndDebugDetails("Error while creating temporary directory for " + originalRepo.getAbsolutePath(), e);
+      FileUtil.delete(gcRepo);
+      return;
+    }
+
+    //swap repositories with write rm lock which guarantees no one uses the original repository
+    Lock rmWriteLock = myRepositoryManager.getRmLock(originalRepo).writeLock();
+    long lockStart = System.currentTimeMillis();
+    rmWriteLock.lock();
+    long lockDuration = System.currentTimeMillis() - lockStart;
+    try {
+      if (!originalRepo.renameTo(oldDir)) {
+        myGcErrors.registerError(originalRepo, "Failed to rename " + originalRepo.getName() + " to " + oldDir.getName());
+        LOG.warn("Failed to rename " + originalRepo.getName() + " to " + oldDir.getName());
+        return;
+      }
+      if (!gcRepo.renameTo(originalRepo)) {
+        myGcErrors.registerError(originalRepo, "Failed to rename " + gcRepo.getName() + " to " + originalRepo.getName());
+        LOG.warn("Failed to rename " + gcRepo.getName() + " to " + originalRepo.getName() + ", will try restoring old repository");
+        if (!oldDir.renameTo(originalRepo)) {
+          LOG.warn("Failed to rename " + oldDir.getName() + " to " + originalRepo.getName());
+        }
+        return;
+      }
+    } finally {
+      rmWriteLock.unlock();
+      FileUtil.delete(oldDir);
+      FileUtil.delete(gcRepo);
+    }
+    long swapDuration = System.currentTimeMillis() - swapStart;
+    if (swapDuration > TimeUnit.SECONDS.toMillis(5)) {
+      String msg = "[" + originalRepo.getName() + "] swap with compacted repository finished in " + swapDuration + "ms";
+      if (lockDuration > TimeUnit.SECONDS.toMillis(1)) {
+        msg += " (lock acquired in " + lockDuration + "ms)";
+      }
+      LOG.info(msg);
+    }
+    myGcErrors.clearError(originalRepo);
+  }
+
+  private void repack(final File gcRepo) throws VcsException {
+    long start = System.currentTimeMillis();
+    GeneralCommandLine cmd = new GeneralCommandLine();
+    cmd.setWorkingDirectory(gcRepo);
+    cmd.setExePath(myConfig.getPathToGit());
+    cmd.addParameter("repack");
+    cmd.addParameters("-a", "-d");
+    ExecResult result = SimpleCommandLineProcessRunner.runCommand(cmd, null, new SimpleCommandLineProcessRunner.RunCommandEventsAdapter() {
+      @Override
+      public Integer getOutputIdleSecondsTimeout() {
+        return myConfig.getRepackIdleTimeoutSeconds();
+      }
+      @Override
+      public void onProcessFinished(@NotNull final Process ps) {
+        LOG.info("[" + gcRepo.getName() + "] 'git repack -a -d' finished in " + (System.currentTimeMillis() - start) + "ms");
+      }
+    });
+    VcsException commandError = CommandLineUtil.getCommandLineError("git repack", result);
+    if (commandError != null) {
+      LOG.warnAndDebugDetails("Error while running 'git repack' in " + gcRepo.getAbsolutePath(), commandError);
+      throw commandError;
+    }
+  }
+
+  private void packRefs(@NotNull File gcRepo) throws VcsException {
+    long start = System.currentTimeMillis();
+    GeneralCommandLine cmd = new GeneralCommandLine();
+    cmd.setWorkingDirectory(gcRepo);
+    cmd.setExePath(myConfig.getPathToGit());
+    cmd.addParameter("pack-refs");
+    cmd.addParameters("--all");
+    ExecResult result = SimpleCommandLineProcessRunner.runCommand(cmd, null, new SimpleCommandLineProcessRunner.RunCommandEventsAdapter() {
+      @Override
+      public Integer getOutputIdleSecondsTimeout() {
+        return myConfig.getPackRefsIdleTimeoutSeconds();
+      }
+      @Override
+      public void onProcessFinished(@NotNull final Process ps) {
+        LOG.info("[" + gcRepo.getName() + "] 'git pack-refs --all' finished in " + (System.currentTimeMillis() - start) + "ms");
+      }
+    });
+    VcsException commandError = CommandLineUtil.getCommandLineError("git pack-refs", result);
+    if (commandError != null) {
+      LOG.warnAndDebugDetails("Error while running 'git pack-refs' in " + gcRepo.getAbsolutePath(), commandError);
+      throw commandError;
+    }
+  }
+
+  private boolean isGcNeeded(@NotNull File gitDir) {
+    FileRepository db = null;
+    try {
+      //implement logic from git gc --auto, jgit version we use doesn't have it yet
+      //and native git doesn't provide a dedicated command for that
+      db = (FileRepository) new RepositoryBuilder().setBare().setGitDir(gitDir).build();
+      return tooManyPacks(db) || tooManyLooseObjects(db);
+    } catch (IOException e) {
+      LOG.warnAndDebugDetails("Error while checking if garbage collection is needed in " + gitDir.getAbsolutePath(), e);
+      return false;
+    } finally {
+      if (db != null)
+        db.close();
+    }
+  }
+
+  private boolean enoughDiskSpaceForGC(@NotNull File gitDir, @Nullable Long freeDiskSpace) {
+    if (freeDiskSpace == null)
+      return true;
+    File objects = new File(gitDir, "objects");
+    File pack = new File(objects, "pack");
+    return FileUtil.getTotalDirectorySize(pack) < freeDiskSpace;
+  }
+
+  private boolean tooManyPacks(@NotNull FileRepository repo) {
+    int limit = repo.getConfig().getInt("gc", "autopacklimit", 50);
+    if (limit <= 0)
+      return false;
+    int packCount = 0;
+    for (PackFile packFile : repo.getObjectDatabase().getPacks()) {
+      if (!packFile.shouldBeKept())
+        packCount++;
+      if (packCount > limit)
+        return true;
+    }
+    return false;
+  }
+
+
+  private boolean tooManyLooseObjects(@NotNull FileRepository repo) {
+    int limit = repo.getConfig().getInt("gc", "auto", 6700);
+    if (limit <= 0)
+      return false;
+    //SHA is evenly distributed, we can estimate number of loose object by counting them in a single bucket (from jgit internals)
+    int bucketLimit = (limit + 255) / 256;
+    File bucket = new File(repo.getObjectsDirectory(), "17");
+    if (!bucket.isDirectory())
+      return false;
+    String[] files = bucket.list();
+    if (files == null)
+      return false;
+    int count = 0;
+    for (String fileName : files) {
+      if (PATTERN_LOOSE_OBJECT.matcher(fileName).matches())
+        count++;
+      if (count > bucketLimit)
+        return true;
+    }
+    return false;
+  }
+
+
+  @NotNull
+  private File setupGcRepo(@NotNull File gitDir) throws IOException {
+    File result = createTempDir(gitDir.getParentFile(), gitDir.getName() + ".gc");
+    Repository repo = new RepositoryBuilder().setBare().setGitDir(result).build();
+    try {
+      repo.create(true);
+    } finally {
+      repo.close();
+    }
+
+    //setup alternates, 'git repack' in a repo with alternates creates a pack
+    //in this repo without affecting the repo alternates point to
+    File objectsDir = new File(result, "objects");
+    File objectsInfo = new File(objectsDir, "info");
+    objectsInfo.mkdirs();
+    FileUtil.writeFileAndReportErrors(new File(objectsInfo, "alternates"), new File(gitDir, "objects").getCanonicalPath());
+
+    copyIfExist(new File(gitDir, "packed-refs"), result);
+    copyIfExist(new File(gitDir, "timestamp"), result);
+    copyDirIfExist(new File(gitDir, "refs"), result);
+    copyDirIfExist(new File(gitDir, "monitoring"), result);
+    return result;
+  }
+
+  private void copyIfExist(@NotNull File srcFile, @NotNull File dstDir) throws IOException {
+    if (srcFile.exists())
+      FileUtil.copy(srcFile, new File(dstDir, srcFile.getName()));
+  }
+
+  private void copyDirIfExist(@NotNull File srcDir, @NotNull File dstDir) throws IOException {
+    if (srcDir.exists())
+      FileUtil.copyDir(srcDir, new File(dstDir, srcDir.getName()));
+  }
+
+  @NotNull
+  private File createTempDir(@NotNull final File parentDir, @NotNull String name) throws IOException {
+    File dir = new File(parentDir, name);
+    if (dir.mkdir())
+      return dir;
+
+    int suffix = 0;
+    while (true) {
+      suffix++;
+      String tmpDirName = name + suffix;
+      dir = new File(parentDir, tmpDirName);
+      if (dir.mkdir())
+        return dir;
+    }
+  }
+
   private void runJGitGC() {
     final long startNanos = System.nanoTime();
     final long gcTimeQuotaNanos = TimeUnit.MINUTES.toNanos(myConfig.getNativeGCQuotaMinutes());
@@ -206,8 +495,11 @@ public class Cleanup {
     ExecResult result = SimpleCommandLineProcessRunner.runCommand(cmd, null);
     VcsException commandError = CommandLineUtil.getCommandLineError("git version", result);
     if (commandError != null) {
-      LOG.info("Cannot run native git", commandError);
+      myNativeGitError.set(new RunGitError(pathToGit, commandError));
+      LOG.warnAndDebugDetails("Failed to run git", commandError);
       return false;
+    } else {
+      myNativeGitError.set(null);
     }
     return true;
   }
@@ -262,14 +554,44 @@ public class Cleanup {
 
       VcsException commandError = CommandLineUtil.getCommandLineError("'git --git-dir=" + bareGitDir.getAbsolutePath() + " gc'", result);
       if (commandError != null) {
-        LOG.error("Error while running 'git --git-dir=" + bareGitDir.getAbsolutePath() + " gc'", commandError);
+        LOG.warnAndDebugDetails("Error while running 'git --git-dir=" + bareGitDir.getAbsolutePath() + " gc'", commandError);
       }
       if (result.getStderr().length() > 0) {
         LOG.debug("Output produced by 'git --git-dir=" + bareGitDir.getAbsolutePath() + " gc'");
         LOG.debug(result.getStderr());
       }
     } catch (Exception e) {
-      LOG.error("Error while running 'git --git-dir=" + bareGitDir.getAbsolutePath() + " gc'", e);
+      myGcErrors.registerError(bareGitDir, e);
+      LOG.warnAndDebugDetails("Error while running 'git --git-dir=" + bareGitDir.getAbsolutePath() + " gc'", e);
+    }
+  }
+
+
+  @Nullable
+  public RunGitError getNativeGitError() {
+    return myNativeGitError.get();
+  }
+
+  public static class RunGitError extends Pair<String, VcsException> {
+    public RunGitError(@NotNull String gitPath, @NotNull VcsException error) {
+      super(gitPath, error);
     }
+
+    @NotNull
+    public String getGitPath() {
+      return first;
+    }
+
+    @NotNull
+    public VcsException getError() {
+      return second;
+    }
+  }
+
+  @NotNull
+  private String pluralize(@NotNull String base, long n) {
+    //StringUtil doesn't work with longs
+    if (n == 1) return base;
+    return StringUtil.pluralize(base);
   }
 }
index 4ff6943f567919dd233d2d388750244c258e4158..cd1fbc537f39051e975ad2aea1e6828001040714 100644 (file)
@@ -21,45 +21,93 @@ import org.jetbrains.annotations.NotNull;
 import org.quartz.CronExpression;
 
 import java.util.Date;
+import java.util.Objects;
 import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.ReentrantLock;
 
-public class CleanupRunner implements Runnable {
+public class CleanupRunner {
 
-  private final RepositoryManager myRepositoryManager;
   private final ServerPluginConfig myConfig;
   private final ScheduledExecutorService myExecutor;
-  private volatile boolean myFirstRun = true;
+  private final Cleanup myCleanup;
+  private final AtomicReference<String> myCron = new AtomicReference<>();
+  private final AtomicReference<ScheduledFuture<?>> myCleanupFeature = new AtomicReference<>();
+  //lock is used to have only one scheduled clean-up task
+  private final ReentrantLock myLock = new ReentrantLock();
 
   public CleanupRunner(@NotNull final ExecutorServices executor,
                        @NotNull final ServerPluginConfig config,
-                       @NotNull final RepositoryManager repositoryManager) {
+                       @NotNull final Cleanup cleanup) {
     myExecutor = executor.getNormalExecutorService();
     myConfig = config;
-    myRepositoryManager = repositoryManager;
-    myExecutor.submit(this);
+    myCleanup = cleanup;
+    myExecutor.scheduleAtFixedRate(this::updateSchedule, 10, 10, TimeUnit.MINUTES);
   }
 
-  public void run() {
-    if (myFirstRun) {
-      myFirstRun = false;
-    } else {
-      new Cleanup(myConfig, myRepositoryManager).run();
+
+  /**
+   * Schedules a new cleanup task if cron expression was changed
+   */
+  private void updateSchedule() {
+    if (!myLock.tryLock())
+      return;
+    try {
+      CronExpression cron = myConfig.getCleanupCronExpression();
+      String cronStr = cron != null ? cron.getCronExpression() : null;
+      if (Objects.equals(myCron.get(), cronStr))
+        return;
+
+      ScheduledFuture<?> feature = myCleanupFeature.get();
+      if (feature != null) {
+        feature.cancel(false);
+      }
+
+      if (cron != null) {
+        schedule(cron);
+      } else {
+        myCleanupFeature.set(null);
+        myCron.set(null);
+      }
+    } finally {
+      myLock.unlock();
     }
-    schedule();
   }
 
-  private void schedule() {
-    CronExpression cron = myConfig.getCleanupCronExpression();
-    if (cron != null) {
-      Date now = new Date();
-      Date next = cron.getNextValidTimeAfter(now);
-      myExecutor.schedule(this, next.getTime() - now.getTime(), TimeUnit.MILLISECONDS);
-    } else {
-      //schedule ourselves to check if the cron expression specified
-      myExecutor.schedule(this, 10, TimeUnit.MINUTES);
-      //do not run cleanup next time, just schedule
-      myFirstRun = true;
+
+  /**
+   * Runs cleanup and schedules itself
+   */
+  private void runCleanup() {
+    myCleanup.run();
+
+    myLock.lock();
+    try {
+      CronExpression cron = myConfig.getCleanupCronExpression();
+      //schedule next clean-up task only if cron expression wasn't changed,
+      //changed cron handled by updateSchedule task
+      if (cron != null && Objects.equals(myCron.get(), cron.getCronExpression())) {
+        schedule(cron);
+      } else {
+        //reset future & cron to avoid ABA problem in updateSchedule(): cron is A,
+        //updateSchedule() remembers it, cron becomes B, runCleanup() doesn't schedule,
+        //cron is changed back to A, updateSchedule() also doesn't schedule
+        myCleanupFeature.set(null);
+        myCron.set(null);
+      }
+    } finally {
+      myLock.unlock();
     }
   }
+
+
+  private void schedule(@NotNull CronExpression cron) {
+    Date now = new Date();
+    Date next = cron.getNextValidTimeAfter(now);
+    ScheduledFuture<?> future = myExecutor.schedule(this::runCleanup, next.getTime() - now.getTime(), TimeUnit.MILLISECONDS);
+    myCleanupFeature.set(future);
+    myCron.set(cron.getCronExpression());
+  }
 }
index d9a919f9ab1f3da9fcf1c1d921a6d0f00509e669..73514d19c3dba13906b738b2c7fcb1534a20385f 100644 (file)
@@ -33,7 +33,6 @@ import java.io.IOException;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.concurrent.locks.Lock;
 
 import static java.util.Arrays.asList;
 
@@ -92,20 +91,14 @@ public class CommitLoaderImpl implements CommitLoader {
                     @NotNull FetchSettings settings) throws IOException, VcsException {
     File repositoryDir = db.getDirectory();
     assert repositoryDir != null : "Non-local repository";
-    Lock rmLock = myRepositoryManager.getRmLock(repositoryDir).readLock();
-    rmLock.lock();
-    try {
-      final long start = System.currentTimeMillis();
-      synchronized (myRepositoryManager.getWriteLock(repositoryDir)) {
-        final long finish = System.currentTimeMillis();
-        Map<String, Ref> oldRefs = new HashMap<String, Ref>(db.getAllRefs());
-        PERFORMANCE_LOG.debug("[waitForWriteLock] repository: " + repositoryDir.getAbsolutePath() + ", took " + (finish - start) + "ms");
-        myFetchCommand.fetch(db, fetchURI, refspecs, settings);
-        Map<String, Ref> newRefs = new HashMap<String, Ref>(db.getAllRefs());
-        myMapFullPath.invalidateRevisionsCache(db, oldRefs, newRefs);
-      }
-    } finally {
-      rmLock.unlock();
+    final long start = System.currentTimeMillis();
+    synchronized (myRepositoryManager.getWriteLock(repositoryDir)) {
+      final long finish = System.currentTimeMillis();
+      Map<String, Ref> oldRefs = new HashMap<String, Ref>(db.getAllRefs());
+      PERFORMANCE_LOG.debug("[waitForWriteLock] repository: " + repositoryDir.getAbsolutePath() + ", took " + (finish - start) + "ms");
+      myFetchCommand.fetch(db, fetchURI, refspecs, settings);
+      Map<String, Ref> newRefs = new HashMap<String, Ref>(db.getAllRefs());
+      myMapFullPath.invalidateRevisionsCache(db, oldRefs, newRefs);
     }
   }
 
diff --git a/git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/GcErrors.java b/git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/GcErrors.java
new file mode 100644 (file)
index 0000000..f1ee06f
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2000-2017 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;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+public class GcErrors {
+
+  private final ConcurrentMap<File, String> myErrors = new ConcurrentHashMap<>();
+
+  void registerError(@NotNull File cloneDir, @NotNull String error) {
+    myErrors.put(cloneDir, error);
+  }
+
+  void registerError(@NotNull File cloneDir, @NotNull Exception error) {
+    myErrors.put(cloneDir, error.toString());
+  }
+
+  void registerError(@NotNull File cloneDir, @NotNull String description, @NotNull Exception error) {
+    myErrors.put(cloneDir, description + " " + error.toString());
+  }
+
+  void clearError(@NotNull File cloneDir) {
+    myErrors.remove(cloneDir);
+  }
+
+  public void retainErrors(@NotNull Collection<File> files) {
+    myErrors.keySet().retainAll(new HashSet<>(files));
+  }
+
+  @NotNull
+  public Map<File, String> getErrors() {
+    return new HashMap<>(myErrors);
+  }
+}
index dd7cd4d29fd421edd7960b0ff7a9bbfa48dbe668..0ec3d0ecf5f7c4f69852b8442f14ad44b4938492 100644 (file)
@@ -44,15 +44,18 @@ public class GitCollectChangesPolicy implements CollectChangesBetweenRepositorie
   private final VcsOperationProgressProvider myProgressProvider;
   private final CommitLoader myCommitLoader;
   private final ServerPluginConfig myConfig;
+  private final RepositoryManager myRepositoryManager;
 
   public GitCollectChangesPolicy(@NotNull GitVcsSupport vcs,
                                  @NotNull VcsOperationProgressProvider progressProvider,
                                  @NotNull CommitLoader commitLoader,
-                                 @NotNull ServerPluginConfig config) {
+                                 @NotNull ServerPluginConfig config,
+                                 @NotNull RepositoryManager repositoryManager) {
     myVcs = vcs;
     myProgressProvider = progressProvider;
     myCommitLoader = commitLoader;
     myConfig = config;
+    myRepositoryManager = repositoryManager;
   }
 
 
@@ -70,29 +73,32 @@ public class GitCollectChangesPolicy implements CollectChangesBetweenRepositorie
                                                @NotNull RepositoryStateData fromState,
                                                @NotNull RepositoryStateData toState,
                                                @NotNull CheckoutRules checkoutRules) throws VcsException {
-    List<ModificationData> changes = new ArrayList<ModificationData>();
     OperationContext context = myVcs.createContext(root, "collecting changes", createProgress());
-    try {
-      Repository r = context.getRepository();
-      ModificationDataRevWalk revWalk = new ModificationDataRevWalk(myConfig, context);
-      revWalk.sort(RevSort.TOPO);
-      ensureRepositoryStateLoadedFor(context, r, true, toState, fromState);
-      markStart(r, revWalk, toState);
-      markUninteresting(r, revWalk, fromState, toState);
-      while (revWalk.next() != null) {
-        changes.add(revWalk.createModificationData());
-      }
-    } catch (Exception e) {
-      if (e instanceof SubmoduleException) {
-        SubmoduleException se = (SubmoduleException) e;
-        Set<String> affectedBranches = getBranchesWithCommit(context.getRepository(), toState, se.getMainRepositoryCommit());
-        throw context.wrapException(se.addBranches(affectedBranches));
+    GitVcsRoot gitRoot = context.getGitRoot();
+    return myRepositoryManager.runWithDisabledRemove(gitRoot.getRepositoryDir(), () -> {
+      List<ModificationData> changes = new ArrayList<ModificationData>();
+      try {
+        Repository r = context.getRepository();
+        ModificationDataRevWalk revWalk = new ModificationDataRevWalk(myConfig, context);
+        revWalk.sort(RevSort.TOPO);
+        ensureRepositoryStateLoadedFor(context, r, true, toState, fromState);
+        markStart(r, revWalk, toState);
+        markUninteresting(r, revWalk, fromState, toState);
+        while (revWalk.next() != null) {
+          changes.add(revWalk.createModificationData());
+        }
+      } catch (Exception e) {
+        if (e instanceof SubmoduleException) {
+          SubmoduleException se = (SubmoduleException) e;
+          Set<String> affectedBranches = getBranchesWithCommit(context.getRepository(), toState, se.getMainRepositoryCommit());
+          throw context.wrapException(se.addBranches(affectedBranches));
+        }
+        throw context.wrapException(e);
+      } finally {
+        context.close();
       }
-      throw context.wrapException(e);
-    } finally {
-      context.close();
-    }
-    return changes;
+      return changes;
+    });
   }
 
 
index 329408fd81fb470290bebcec6e2af26917e13be9..76cc430c3827e045a94f639bd5cc7d3897ce138f 100644 (file)
@@ -39,6 +39,7 @@ import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.*;
+import java.util.concurrent.locks.Lock;
 
 import static java.util.Arrays.asList;
 
@@ -68,8 +69,10 @@ public class GitCommitSupport implements CommitSupport, GitServerExtension {
   @NotNull
   public CommitPatchBuilder getCommitPatchBuilder(@NotNull VcsRoot root) throws VcsException {
     OperationContext context = myVcs.createContext(root, "commit");
+    Lock rmLock = myRepositoryManager.getRmLock(context.getGitRoot().getRepositoryDir()).readLock();
+    rmLock.lock();
     Repository db = context.getRepository();
-    return new GitCommitPatchBuilder(myVcs, context, myCommitLoader, db, myRepositoryManager, myTransportFactory, myPluginConfig);
+    return new GitCommitPatchBuilder(myVcs, context, myCommitLoader, db, myRepositoryManager, myTransportFactory, myPluginConfig, rmLock);
   }
 
 
@@ -84,6 +87,7 @@ public class GitCommitSupport implements CommitSupport, GitServerExtension {
     private final RepositoryManager myRepositoryManager;
     private final TransportFactory myTransportFactory;
     private final ServerPluginConfig myPluginConfig;
+    private final Lock myRmLock;
 
     private GitCommitPatchBuilder(@NotNull GitVcsSupport vcs,
                                   @NotNull OperationContext context,
@@ -91,7 +95,8 @@ public class GitCommitSupport implements CommitSupport, GitServerExtension {
                                   @NotNull Repository db,
                                   @NotNull RepositoryManager repositoryManager,
                                   @NotNull TransportFactory transportFactory,
-                                  @NotNull ServerPluginConfig pluginConfig) {
+                                  @NotNull ServerPluginConfig pluginConfig,
+                                  @NotNull Lock rmLock) {
       myVcs = vcs;
       myContext = context;
       myCommitLoader = commitLoader;
@@ -100,6 +105,7 @@ public class GitCommitSupport implements CommitSupport, GitServerExtension {
       myRepositoryManager = repositoryManager;
       myTransportFactory = transportFactory;
       myPluginConfig = pluginConfig;
+      myRmLock = rmLock;
     }
 
     public void createFile(@NotNull String path, @NotNull InputStream content) throws VcsException {
@@ -312,6 +318,7 @@ public class GitCommitSupport implements CommitSupport, GitServerExtension {
     }
 
     public void dispose() {
+      myRmLock.unlock();
       myContext.close();
     }
   }
index d18e0ac902188c02782e446cb860596b6fdfc8a0..fcc2fe5694d80f5e0778591f1470e3ee4170e441 100644 (file)
@@ -40,13 +40,17 @@ public class GitFetchService implements FetchService, GitServerExtension {
                               @NotNull final CheckoutRules rules,
                               @NotNull final FetchRepositoryCallback callback) throws VcsException {
     final OperationContext ctx = myVcs.createContext(root, "Fetch", new FetchCallbackProgress(callback));
-    try {
-      fetchRepositoryImpl(ctx);
-    } catch (Exception e) {
-      throw ctx.wrapException(e);
-    } finally {
-      ctx.close();
-    }
+    GitVcsRoot gitRoot = ctx.getGitRoot();
+    myVcs.getRepositoryManager().runWithDisabledRemove(gitRoot.getRepositoryDir(), () -> {
+      try {
+        fetchRepositoryImpl(ctx);
+      } catch (Exception e) {
+        throw ctx.wrapException(e);
+      } finally {
+        ctx.close();
+      }
+    });
+
   }
 
   @NotNull
index 227def3ad7aa0f73bcad174fc056ca8240d903eb..c217a55119a2a465a92e98ca3d80ae3df4771564 100644 (file)
@@ -31,7 +31,6 @@ import org.jetbrains.annotations.Nullable;
 
 import java.io.IOException;
 import java.util.*;
-import java.util.concurrent.locks.ReadWriteLock;
 
 import static com.intellij.openapi.util.text.StringUtil.isEmpty;
 import static java.util.Arrays.asList;
@@ -68,44 +67,43 @@ public class GitLabelingSupport implements LabelingSupport {
                       @NotNull CheckoutRules checkoutRules) throws VcsException {
     OperationContext context = myVcs.createContext(root, "labeling");
     GitVcsRoot gitRoot = context.getGitRoot();
-    RevisionsInfo revisionsInfo = new RevisionsInfo();
-    if (myConfig.useTagPackHeuristics()) {
-      LOG.debug("Update repository before labeling " + gitRoot.debugInfo());
-      RepositoryStateData currentState = myVcs.getCurrentState(gitRoot);
-      if (!myConfig.analyzeTagsInPackHeuristics())
-        currentState = excludeTags(currentState);
+    return myRepositoryManager.runWithDisabledRemove(gitRoot.getRepositoryDir(), () -> {
+      RevisionsInfo revisionsInfo = new RevisionsInfo();
+      if (myConfig.useTagPackHeuristics()) {
+        LOG.debug("Update repository before labeling " + gitRoot.debugInfo());
+        RepositoryStateData currentState = myVcs.getCurrentState(gitRoot);
+        if (!myConfig.analyzeTagsInPackHeuristics())
+          currentState = excludeTags(currentState);
+        try {
+          myVcs.getCollectChangesPolicy().ensureRepositoryStateLoadedFor(context, context.getRepository(), false, currentState);
+        } catch (Exception e) {
+          LOG.debug("Error while updating repository " + gitRoot.debugInfo(), e);
+        }
+        revisionsInfo = new RevisionsInfo(currentState);
+      }
       try {
-        myVcs.getCollectChangesPolicy().ensureRepositoryStateLoadedFor(context, context.getRepository(), false, currentState);
+        long start = System.currentTimeMillis();
+        Repository r = context.getRepository();
+        String commitSHA = GitUtils.versionRevision(version);
+        RevCommit commit = myCommitLoader.loadCommit(context, gitRoot, commitSHA);
+        Git git = new Git(r);
+        Ref tagRef = git.tag().setTagger(gitRoot.getTagger(r))
+          .setName(label)
+          .setObjectId(commit)
+          .call();
+        if (tagRef.getObjectId() == null || resolve(r, tagRef) == null) {
+          LOG.warn("Tag's " + tagRef.getName() + " objectId " + (tagRef.getObjectId() != null ? tagRef.getObjectId().name() + " " : "") + "cannot be resolved");
+        } else if (LOG.isDebugEnabled()) {
+          LOG.debug("Tag created  " + label + "=" + version + " for " + gitRoot.debugInfo() +
+                    " in " + (System.currentTimeMillis() - start) + "ms");
+        }
+        return push(label, version, gitRoot, r, tagRef, revisionsInfo);
       } catch (Exception e) {
-        LOG.debug("Error while updating repository " + gitRoot.debugInfo(), e);
-      }
-      revisionsInfo = new RevisionsInfo(currentState);
-    }
-    ReadWriteLock rmLock = myRepositoryManager.getRmLock(gitRoot.getRepositoryDir());
-    rmLock.readLock().lock();
-    try {
-      long start = System.currentTimeMillis();
-      Repository r = context.getRepository();
-      String commitSHA = GitUtils.versionRevision(version);
-      RevCommit commit = myCommitLoader.loadCommit(context, gitRoot, commitSHA);
-      Git git = new Git(r);
-      Ref tagRef = git.tag().setTagger(gitRoot.getTagger(r))
-        .setName(label)
-        .setObjectId(commit)
-        .call();
-      if (tagRef.getObjectId() == null || resolve(r, tagRef) == null) {
-        LOG.warn("Tag's " + tagRef.getName() + " objectId " + (tagRef.getObjectId() != null ? tagRef.getObjectId().name() + " " : "") + "cannot be resolved");
-      } else if (LOG.isDebugEnabled()) {
-        LOG.debug("Tag created  " + label + "=" + version + " for " + gitRoot.debugInfo() +
-                  " in " + (System.currentTimeMillis() - start) + "ms");
+        throw context.wrapException(e);
+      } finally {
+        context.close();
       }
-      return push(label, version, gitRoot, r, tagRef, revisionsInfo);
-    } catch (Exception e) {
-      throw context.wrapException(e);
-    } finally {
-      rmLock.readLock().unlock();
-      context.close();
-    }
+    });
   }
 
   @NotNull
index a9db990c3c8e4ebe6d9a3abddd0512138f9d92ee..baa08fd4ed67cee584ef134dac59afd15040f345 100644 (file)
@@ -38,6 +38,7 @@ import java.net.URISyntaxException;
 import java.util.*;
 
 import static com.intellij.openapi.util.text.StringUtil.isEmpty;
+import static java.util.Collections.singleton;
 
 /**
 * @author kir
@@ -61,56 +62,64 @@ public class GitMapFullPath {
 
 
   @NotNull
-  public Collection<String> mapFullPath(@NotNull OperationContext context, @NotNull VcsRootEntry rootEntry, @NotNull String path) throws VcsException, IOException {
-    GitVcsRoot root = context.getGitRoot();
+  public Collection<String> mapFullPath(@NotNull OperationContext context, @NotNull VcsRootEntry rootEntry, @NotNull String path) throws VcsException {
+    GitVcsRoot root = context.getGitRoot(rootEntry.getVcsRoot());
     if (LOG.isDebugEnabled())
       LOG.debug("MapFullPath root: " + LogUtil.describe(root) + ", path " + path);
     FullPath fullPath = new FullPath(path);
-    if (!fullPath.isValid()) {
-      LOG.warn("Invalid path: " + path);
+    if (repositoryContainsPath(context, root, fullPath)) {
+      return fullPath.getMappedPaths();
+    } else {
       return Collections.emptySet();
     }
+  }
+
 
-    //match by revision
-    if (fullPath.containsRevision()) {
-      if (fullPath.containsHintRevision()) {
-        //if full path has a hint revision, first check if repository contains it;
-        //a hint revision should rarely change and most likely will be cached
-        if (repositoryContainsRevision(context, rootEntry, fullPath.getHintRevision(), RevisionCacheType.HINT_CACHE)
-            && repositoryContainsRevision(context, rootEntry, fullPath.getRevision(), RevisionCacheType.COMMIT_CACHE))
-            return fullPath.getMappedPaths();
+  boolean repositoryContainsPath(@NotNull OperationContext context,
+                                 @NotNull GitVcsRoot root,
+                                 @NotNull FullPath path) throws VcsException {
+    if (!path.isValid()) {
+      LOG.warn("Invalid path: " + path);
+      return false;
+    }
+
+    try {
+      if (path.containsRevision()) {
+        if (path.containsHintRevision()) {
+          //if full path has a hint revision, first check if repository contains it;
+          //a hint revision should rarely change and most likely will be cached
+          return repositoryContainsRevision(context, root, path.getHintRevision(), RevisionCacheType.HINT_CACHE) &&
+                 repositoryContainsRevision(context, root, path.getRevision(), RevisionCacheType.COMMIT_CACHE);
+        } else {
+          return repositoryContainsRevision(context, root, path.getRevision(), RevisionCacheType.COMMIT_CACHE);
+        }
       } else {
-        if (repositoryContainsRevision(context, rootEntry, fullPath.getRevision(), RevisionCacheType.COMMIT_CACHE))
-          return fullPath.getMappedPaths();
+        return urlsMatch(root, path);
       }
+    } catch (IOException e) {
+      LOG.error("Error while checking path suitability for root " + LogUtil.describe(root) + ", path: " + path.getPath(), e);
+      return false;
     }
-
-    //match by url only if path doesn't have revision
-    if (!fullPath.containsRevision() && urlsMatch(root, fullPath))
-      return fullPath.getMappedPaths();
-
-    return Collections.emptySet();
   }
 
 
   private boolean repositoryContainsRevision(@NotNull OperationContext context,
-                                             @NotNull VcsRootEntry rootEntry,
+                                             @NotNull GitVcsRoot root,
                                              @NotNull String revision,
                                              @NotNull RevisionCacheType type) throws VcsException, IOException {
-    GitVcsRoot root = context.getGitRoot();
     RepositoryRevisionCache repositoryCache = myCache.getRepositoryCache(root.getRepositoryDir(), type);
     long resetCounter = repositoryCache.getResetCounter();
     Boolean hasRevision = repositoryCache.hasRevision(revision);
     if (hasRevision != null) {
       if (LOG.isDebugEnabled())
-        LOG.debug("RevisionCache hit: root " + LogUtil.describe(rootEntry.getVcsRoot()) + (hasRevision ? "contains " : "doesn't contain ") + "revision " + revision);
+        LOG.debug("RevisionCache hit: root " + LogUtil.describe(root) + (hasRevision ? "contains " : "doesn't contain ") + "revision " + revision);
       return hasRevision;
     } else {
       if (LOG.isDebugEnabled())
-        LOG.debug("RevisionCache miss: root " + LogUtil.describe(rootEntry.getVcsRoot()) + ", revision " + revision + ", lookup commit in repository");
-      hasRevision = myCommitLoader.findCommit(context.getRepository(), revision) != null;
+        LOG.debug("RevisionCache miss: root " + LogUtil.describe(root) + ", revision " + revision + ", lookup commit in repository");
+      hasRevision = myCommitLoader.findCommit(context.getRepository(root), revision) != null;
       if (LOG.isDebugEnabled())
-        LOG.debug("Root " + LogUtil.describe(rootEntry.getVcsRoot()) + ", revision " + revision + (hasRevision ? " was found" : " wasn't found") + ", cache the result");
+        LOG.debug("Root " + LogUtil.describe(root) + ", revision " + revision + (hasRevision ? " was found" : " wasn't found") + ", cache the result");
       repositoryCache.saveRevision(revision, hasRevision, resetCounter);
       return hasRevision;
     }
@@ -209,7 +218,7 @@ public class GitMapFullPath {
 
 
   //Format: <hint revision>-<git revision hash>|<repository url>|<file relative path>
-  private static class FullPath {
+  public static class FullPath {
     private final String myPath;
     private final int myFirstSeparatorIdx;
     private final int myLastSeparatorIdx;
@@ -217,7 +226,7 @@ public class GitMapFullPath {
     private final String myRevision;
     private final String myHintRevision;
 
-    private FullPath(@NotNull String path) {
+    public FullPath(@NotNull String path) {
       myPath = path;
       myFirstSeparatorIdx = path.indexOf("|");
       myLastSeparatorIdx = path.lastIndexOf("|");
@@ -272,7 +281,25 @@ public class GitMapFullPath {
 
     @NotNull
     Collection<String> getMappedPaths() {
-      return Collections.singleton(myPath.substring(myLastSeparatorIdx + 1).trim());
+      return singleton(myPath.substring(myLastSeparatorIdx + 1).trim());
+    }
+
+    @NotNull
+    public String getPath() {
+      return myPath;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+      if (this == o) return true;
+      if (!(o instanceof FullPath)) return false;
+      final FullPath fullPath = (FullPath)o;
+      return myPath.equals(fullPath.myPath);
+    }
+
+    @Override
+    public int hashCode() {
+      return myPath.hashCode();
     }
   }
 }
index 59caa23f70dcc27608a16e94fff7b8853939aec7..05323ff4a9807ba1a7a34e132942068061eebc8d 100644 (file)
@@ -68,34 +68,36 @@ public class GitMergeSupport implements MergeSupport, GitServerExtension {
                            @NotNull MergeOptions options) throws VcsException {
     LOG.info("Merge in root " + root + ", revision " + srcRevision + ", destination " + dstBranch);
     OperationContext context = myVcs.createContext(root, "merge");
-    try {
-      GitVcsRoot gitRoot = context.getGitRoot();
-      Repository db = context.getRepository();
-      int attemptsLeft = myPluginConfig.getMergeRetryAttempts();
-      MergeResult result;
-      do {
-        try {
-          result = doMerge(context, gitRoot, db, srcRevision, dstBranch, message, options);
-          if (result.isMergePerformed() && result.isSuccess()) {
-            LOG.info("Merge successfully finished in root " + root + ", revision " + srcRevision + ", destination " + dstBranch);
-            return result;
+    GitVcsRoot gitRoot = context.getGitRoot();
+    return myRepositoryManager.runWithDisabledRemove(gitRoot.getRepositoryDir(), () -> {
+      try {
+        Repository db = context.getRepository();
+        int attemptsLeft = myPluginConfig.getMergeRetryAttempts();
+        MergeResult result;
+        do {
+          try {
+            result = doMerge(context, gitRoot, db, srcRevision, dstBranch, message, options);
+            if (result.isMergePerformed() && result.isSuccess()) {
+              LOG.info("Merge successfully finished in root " + root + ", revision " + srcRevision + ", destination " + dstBranch);
+              return result;
+            }
+            attemptsLeft--;
+            LOG.info("Merge was not successful, root " + root + ", revision " + srcRevision + ", destination " + dstBranch + ", attempts left " + attemptsLeft);
+          } catch (IOException e) {
+            LOG.info("Merge failed, root " + root + ", revision " + srcRevision + ", destination " + dstBranch, e);
+            return MergeResult.createMergeError(e.getMessage());
+          } catch (VcsException e) {
+            LOG.info("Merge failed, root " + root + ", revision " + srcRevision + ", destination " + dstBranch, e);
+            return MergeResult.createMergeError(e.getMessage());
           }
-          attemptsLeft--;
-          LOG.info("Merge was not successful, root " + root + ", revision " + srcRevision + ", destination " + dstBranch + ", attempts left " + attemptsLeft);
-        } catch (IOException e) {
-          LOG.info("Merge failed, root " + root + ", revision " + srcRevision + ", destination " + dstBranch, e);
-          return MergeResult.createMergeError(e.getMessage());
-        } catch (VcsException e) {
-          LOG.info("Merge failed, root " + root + ", revision " + srcRevision + ", destination " + dstBranch, e);
-          return MergeResult.createMergeError(e.getMessage());
-        }
-      } while (attemptsLeft > 0);
-      return result;
-    } catch (Exception e) {
-      throw context.wrapException(e);
-    } finally {
-      context.close();
-    }
+        } while (attemptsLeft > 0);
+        return result;
+      } catch (Exception e) {
+        throw context.wrapException(e);
+      } finally {
+        context.close();
+      }
+    });
   }
 
 
@@ -105,29 +107,32 @@ public class GitMergeSupport implements MergeSupport, GitServerExtension {
                                               @NotNull MergeOptions options) throws VcsException {
     Map<MergeTask, MergeResult> mergeResults = new HashMap<MergeTask, MergeResult>();
     OperationContext context = myVcs.createContext(root, "merge");
-    try {
-      Repository db = context.getRepository();
-      for (MergeTask t : tasks) {
-        ObjectId src = ObjectId.fromString(t.getSourceRevision());
-        ObjectId dst = ObjectId.fromString(t.getDestinationRevision());
-        ResolveMerger merger = (ResolveMerger) MergeStrategy.RECURSIVE.newMerger(db, true);
-        try {
-          boolean success = merger.merge(dst, src);
-          if (success) {
-            mergeResults.put(t, MergeResult.createMergeSuccessResult());
-          } else {
-            mergeResults.put(t, MergeResult.createMergeError(merger.getUnmergedPaths()));
+    GitVcsRoot gitRoot = context.getGitRoot();
+    return myRepositoryManager.runWithDisabledRemove(gitRoot.getRepositoryDir(), () -> {
+      try {
+        Repository db = context.getRepository();
+        for (MergeTask t : tasks) {
+          ObjectId src = ObjectId.fromString(t.getSourceRevision());
+          ObjectId dst = ObjectId.fromString(t.getDestinationRevision());
+          ResolveMerger merger = (ResolveMerger) MergeStrategy.RECURSIVE.newMerger(db, true);
+          try {
+            boolean success = merger.merge(dst, src);
+            if (success) {
+              mergeResults.put(t, MergeResult.createMergeSuccessResult());
+            } else {
+              mergeResults.put(t, MergeResult.createMergeError(merger.getUnmergedPaths()));
+            }
+          } catch (IOException mergeException) {
+            mergeResults.put(t, MergeResult.createMergeError(mergeException.getMessage()));
           }
-        } catch (IOException mergeException) {
-          mergeResults.put(t, MergeResult.createMergeError(mergeException.getMessage()));
         }
+      } catch (Exception e) {
+        throw context.wrapException(e);
+      } finally {
+        context.close();
       }
-    } catch (Exception e) {
-      throw context.wrapException(e);
-    } finally {
-      context.close();
-    }
-    return mergeResults;
+      return mergeResults;
+    });
   }
 
   @NotNull
index 0229678ad3bee5f0ad2a019cd3f158147a2ccda3..e002d8f01b4c40b95133580a0434724bc47dcaac 100644 (file)
@@ -24,6 +24,7 @@ import java.io.File;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.Lock;
 
 import static com.intellij.openapi.util.io.FileUtil.delete;
 import static java.util.Collections.singletonList;
@@ -37,10 +38,13 @@ public class GitResetCacheHandler implements ResetCacheHandler {
   private final static String GIT_CACHE_NAME = "git";
 
   private final RepositoryManager myRepositoryManager;
+  private final GcErrors myGcErrors;
   private AtomicBoolean myResetRunning = new AtomicBoolean(false);
 
-  public GitResetCacheHandler(@NotNull RepositoryManager repositoryManager) {
+  public GitResetCacheHandler(@NotNull RepositoryManager repositoryManager,
+                              @NotNull GcErrors gcErrors) {
     myRepositoryManager = repositoryManager;
+    myGcErrors = gcErrors;
   }
 
   @NotNull
@@ -75,30 +79,20 @@ public class GitResetCacheHandler implements ResetCacheHandler {
     for (Map.Entry<String, File> entry : myRepositoryManager.getMappings().entrySet()) {
       String url = entry.getKey();
       File mirror = entry.getValue();
+      Lock writeLock = myRepositoryManager.getRmLock(mirror).writeLock();
+      writeLock.lock();
       try {
-        lockMirror(url, mirror);
-        resetMirror(mirror);
+        resetMirror(mirror, url);
       } finally {
-        unlockMirror(url, mirror);
+        writeLock.unlock();
       }
     }
     LOG.info("Git caches reset");
   }
 
-  private void lockMirror(@NotNull String url, @NotNull File mirror) {
-    LOG.debug("Lock mirror of " + url);
-    myRepositoryManager.getRmLock(mirror).writeLock().lock();
-    LOG.debug("Mirror of " + url + " is locked");
-  }
-
-  private void resetMirror(@NotNull File mirror) {
-    LOG.debug("Reset git mirror "  + mirror.getAbsolutePath());
+  private void resetMirror(@NotNull File mirror, @NotNull String url) {
+    LOG.debug("Delete of the repository " + url + " (" + mirror.getAbsolutePath() + ")");
     delete(mirror);
-    LOG.debug("Git mirror "  + mirror.getAbsolutePath() + " reset");
-  }
-
-  private void unlockMirror(@NotNull String url, @NotNull File mirror) {
-    myRepositoryManager.getRmLock(mirror).writeLock().unlock();
-    LOG.debug("Mirror of " + url + " is unlocked");
+    myGcErrors.clearError(mirror);
   }
 }
index e28563d964585703e91786107bf58492acef66bb..3edf70cd8f520e3525a86ec3118c525a659fced9 100644 (file)
@@ -83,18 +83,21 @@ public class GitServerUtil {
     try {
       ensureRepositoryIsValid(dir);
       Repository r = new RepositoryBuilder().setBare().setGitDir(dir).build();
+      String remoteUrl = remote.toString();
+      if (remoteUrl.contains("\n") || remoteUrl.contains("\r"))
+        throw new VcsException("Newline in url '" + remoteUrl + "'");
       if (!new File(dir, "config").exists()) {
         r.create(true);
         final StoredConfig config = r.getConfig();
-        config.setString("teamcity", null, "remote", remote.toString());
+        config.setString("teamcity", null, "remote", remoteUrl);
         config.save();
       } else {
         final StoredConfig config = r.getConfig();
         final String existingRemote = config.getString("teamcity", null, "remote");
-        if (existingRemote != null && !remote.toString().equals(existingRemote)) {
+        if (existingRemote != null && !remoteUrl.equals(existingRemote)) {
           throw getWrongUrlError(dir, existingRemote, remote);
         } else if (existingRemote == null) {
-          config.setString("teamcity", null, "remote", remote.toString());
+          config.setString("teamcity", null, "remote", remoteUrl);
           config.save();
         }
       }
index 132ad4feeffbada6c49eacfe16d1d5b3b88a1518..400c5cf1c65c936b25ca1ad9efba319277812e78 100644 (file)
@@ -60,7 +60,12 @@ public class GitUrlSupport implements UrlSupport, PositionAware {
       throw new VcsException(e.getMessage(), e);
     }
 
-    Map<String, String> props = new HashMap<String, String>(myGitSupport.getDefaultVcsProperties());
+    if (fetchUrl.startsWith("https://") && !fetchUrl.endsWith(".git") && uri.getHost().contains("gitlab.com")) {
+      // for GitLab we need to add .git suffix to the fetch URL, otherwise, for some reason JGit can't work with this repository (although regular git command works)
+      fetchUrl = fetchUrl + ".git";
+    }
+
+    Map<String, String> props = new HashMap<>(myGitSupport.getDefaultVcsProperties());
     props.put(Constants.FETCH_URL, fetchUrl);
     props.putAll(getAuthSettings(url, uri));
 
@@ -77,6 +82,8 @@ public class GitUrlSupport implements UrlSupport, PositionAware {
     } catch (VcsException e) {
       if (GitServerUtil.isAuthError(e))
         throw e;
+      if (GitVcsSupport.GIT_REPOSITORY_HAS_NO_BRANCHES.equals(e.getMessage()))
+        throw e;
       return null; // probably not git
     }
   }
index 208c291bd8dcbb5590ec13597f9e089d254afe11..9ecab3fc83792ce3263264319cf0d07825c6b74c 100755 (executable)
@@ -36,8 +36,10 @@ import org.eclipse.jgit.transport.URIish;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import java.io.File;
 import java.io.IOException;
 import java.util.*;
+import java.util.stream.Collectors;
 
 import static jetbrains.buildServer.buildTriggers.vcs.git.GitServerUtil.friendlyNotSupportedException;
 import static jetbrains.buildServer.buildTriggers.vcs.git.GitServerUtil.friendlyTransportException;
@@ -50,11 +52,13 @@ import static jetbrains.buildServer.util.CollectionsUtil.setOf;
  * Git VCS support
  */
 public class GitVcsSupport extends ServerVcsSupport
-  implements VcsPersonalSupport, BuildPatchByCheckoutRules,
+  implements VcsBulkSuitabilityChecker, BuildPatchByCheckoutRules,
              TestConnectionSupport, IncludeRuleBasedMappingProvider {
 
   private static final Logger LOG = Logger.getInstance(GitVcsSupport.class.getName());
   private static final Logger PERFORMANCE_LOG = Logger.getInstance(GitVcsSupport.class.getName() + ".Performance");
+  static final String GIT_REPOSITORY_HAS_NO_BRANCHES = "Git repository has no branches";
+
   private ExtensionHolder myExtensionHolder;
   private volatile String myDisplayName = null;
   private final ServerPluginConfig myConfig;
@@ -147,20 +151,26 @@ public class GitVcsSupport extends ServerVcsSupport
 
   @NotNull
   public RepositoryStateData getCurrentState(@NotNull GitVcsRoot gitRoot) throws VcsException {
-    String refInRoot = gitRoot.getRef();
-    String fullRef = GitUtils.expandRef(refInRoot);
-    Map<String, String> branchRevisions = new HashMap<String, String>();
-    for (Ref ref : getRemoteRefs(gitRoot.getOriginalRoot()).values()) {
-      if (!ref.getName().startsWith("ref"))
-        continue;
-      if (!gitRoot.isReportTags() && isTag(ref) && !fullRef.equals(ref.getName()))
-        continue;
-      branchRevisions.put(ref.getName(), getRevision(ref));
-    }
-    if (branchRevisions.get(fullRef) == null && !gitRoot.isIgnoreMissingDefaultBranch()) {
-      throw new VcsException("Cannot find revision of the default branch '" + refInRoot + "' of vcs root " + LogUtil.describe(gitRoot));
-    }
-    return RepositoryStateData.createVersionState(fullRef, branchRevisions);
+    return myRepositoryManager.runWithDisabledRemove(gitRoot.getRepositoryDir(), () -> {
+      String refInRoot = gitRoot.getRef();
+      String fullRef = GitUtils.expandRef(refInRoot);
+      Map<String, String> branchRevisions = new HashMap<String, String>();
+      for (Ref ref : getRemoteRefs(gitRoot.getOriginalRoot()).values()) {
+        if (!ref.getName().startsWith("ref"))
+          continue;
+        if (!gitRoot.isReportTags() && isTag(ref) && !fullRef.equals(ref.getName()))
+          continue;
+        branchRevisions.put(ref.getName(), getRevision(ref));
+      }
+      if (branchRevisions.get(fullRef) == null && !gitRoot.isIgnoreMissingDefaultBranch()) {
+        if (branchRevisions.isEmpty()) {
+          throw new VcsException(GIT_REPOSITORY_HAS_NO_BRANCHES);
+        } else {
+          throw new VcsException("Cannot find revision of the default branch '" + refInRoot + "' of vcs root '" + gitRoot.getName() + "'");
+        }
+      }
+      return RepositoryStateData.createVersionState(fullRef, branchRevisions);
+    });
   }
 
   public void buildPatch(@NotNull VcsRoot root,
@@ -172,16 +182,18 @@ public class GitVcsSupport extends ServerVcsSupport
     String fromRevision = fromVersion != null ? GitUtils.versionRevision(fromVersion) : null;
     String toRevision = GitUtils.versionRevision(toVersion);
     logBuildPatch(root, fromRevision, toRevision);
-    GitPatchBuilderDispatcher
-      gitPatchBuilder = new GitPatchBuilderDispatcher(myConfig, mySshKeyManager, context, builder, fromRevision, toRevision, checkoutRules);
-    try {
-      myCommitLoader.loadCommit(context, context.getGitRoot(), toRevision);
-      gitPatchBuilder.buildPatch();
-    } catch (Exception e) {
-      throw context.wrapException(e);
-    } finally {
-      context.close();
-    }
+    GitVcsRoot gitRoot = context.getGitRoot();
+    myRepositoryManager.runWithDisabledRemove(gitRoot.getRepositoryDir(), () -> {
+      GitPatchBuilderDispatcher gitPatchBuilder = new GitPatchBuilderDispatcher(myConfig, mySshKeyManager, context, builder, fromRevision, toRevision, checkoutRules);
+      try {
+        myCommitLoader.loadCommit(context, gitRoot, toRevision);
+        gitPatchBuilder.buildPatch();
+      } catch (Exception e) {
+        throw context.wrapException(e);
+      } finally {
+        context.close();
+      }
+    });
   }
 
   private void logBuildPatch(@NotNull VcsRoot root, @Nullable String fromRevision, @NotNull String toRevision) {
@@ -274,14 +286,17 @@ public class GitVcsSupport extends ServerVcsSupport
 
   public String testConnection(@NotNull VcsRoot vcsRoot) throws VcsException {
     OperationContext context = createContext(vcsRoot, "connection test");
-    TestConnectionCommand command = new TestConnectionCommand(this, myTransportFactory, myRepositoryManager);
-    try {
-      return command.testConnection(context);
-    } catch (Exception e) {
-      throw context.wrapException(e);
-    } finally {
-      context.close();
-    }
+    GitVcsRoot gitRoot = context.getGitRoot();
+    return myRepositoryManager.runWithDisabledRemove(gitRoot.getRepositoryDir(), () -> {
+      TestConnectionCommand command = new TestConnectionCommand(this, myTransportFactory, myRepositoryManager);
+      try {
+        return command.testConnection(context);
+      } catch (Exception e) {
+        throw context.wrapException(e);
+      } finally {
+        context.close();
+      }
+    });
   }
 
 
@@ -290,11 +305,15 @@ public class GitVcsSupport extends ServerVcsSupport
     return this;
   }
 
+  public OperationContext createContext(@NotNull String operation) {
+    return createContext(null, operation);
+  }
+
   public OperationContext createContext(VcsRoot root, String operation) {
     return createContext(root, operation, GitProgress.NO_OP);
   }
 
-  public OperationContext createContext(@NotNull VcsRoot root, @NotNull String operation, @NotNull GitProgress progress) {
+  public OperationContext createContext(@Nullable VcsRoot root, @NotNull String operation, @NotNull GitProgress progress) {
     return new OperationContext(myCommitLoader, myRepositoryManager, root, operation, progress);
   }
 
@@ -310,7 +329,7 @@ public class GitVcsSupport extends ServerVcsSupport
 
   @NotNull
   public GitCollectChangesPolicy getCollectChangesPolicy() {
-    return new GitCollectChangesPolicy(this, myProgressProvider, myCommitLoader, myConfig);
+    return new GitCollectChangesPolicy(this, myProgressProvider, myCommitLoader, myConfig, myRepositoryManager);
   }
 
   @NotNull
@@ -336,7 +355,8 @@ public class GitVcsSupport extends ServerVcsSupport
   public Collection<String> mapFullPath(@NotNull final VcsRootEntry rootEntry, @NotNull final String fullPath) {
     OperationContext context = createContext(rootEntry.getVcsRoot(), "map full path");
     try {
-      return myMapFullPath.mapFullPath(context, rootEntry, fullPath);
+      return myRepositoryManager.runWithDisabledRemove(context.getGitRoot().getRepositoryDir(), () ->
+        myMapFullPath.mapFullPath(context, rootEntry, fullPath));
     } catch (VcsException e) {
       LOG.warnAndDebugDetails("Error while mapping path for root " + LogUtil.describe(rootEntry.getVcsRoot()), e);
       return Collections.emptySet();
@@ -348,6 +368,67 @@ public class GitVcsSupport extends ServerVcsSupport
     }
   }
 
+
+  @NotNull
+  @Override
+  public List<Boolean> checkSuitable(@NotNull List<VcsRootEntry> entries, @NotNull Collection<String> paths) throws VcsException {
+    OperationContext context = createContext("checkSuitable");
+    try {
+      Set<GitMapFullPath.FullPath> fullPaths = paths.stream().map(GitMapFullPath.FullPath::new).collect(Collectors.toSet());
+
+      //checkout rules do not affect suitability, we can check it for unique root only ignoring different checkout rules
+      Set<VcsRoot> uniqueRoots = entries.stream().map(VcsRootEntry::getVcsRoot).collect(Collectors.toSet());
+      Set<GitVcsRoot> gitRoots = new HashSet<>();
+      for (VcsRoot root : uniqueRoots) {
+        try {
+          gitRoots.add(context.getGitRoot(root));
+        } catch (VcsException e) {
+          //will return false for broken VCS root
+          LOG.warnAndDebugDetails("Error while checking suitability for root " + LogUtil.describe(root) + ", assume root is not suitable", e);
+        }
+      }
+
+      //several roots with different settings can be cloned into the same dir,
+      //do not compute suitability for given clone dir more than once
+      Map<File, Boolean> cloneDirResults = new HashMap<>();//clone dir -> result for this dir
+      Map<VcsRoot, Boolean> rootResult = new HashMap<>();
+      for (GitVcsRoot gitRoot : gitRoots) {
+        File cloneDir = gitRoot.getRepositoryDir();
+        Boolean cloneDirResult = cloneDirResults.get(cloneDir);
+        if (cloneDirResult != null) {
+          rootResult.put(gitRoot.getOriginalRoot(), cloneDirResult);
+          continue;
+        }
+
+        boolean suitable = myRepositoryManager.runWithDisabledRemove(cloneDir, () -> {
+          for (GitMapFullPath.FullPath path : fullPaths) {
+            if (myMapFullPath.repositoryContainsPath(context, gitRoot, path))
+              return true;
+          }
+          return false;
+        });
+
+        rootResult.put(gitRoot.getOriginalRoot(), suitable);
+        cloneDirResults.put(gitRoot.getRepositoryDir(), suitable);
+      }
+
+      List<Boolean> result = new ArrayList<>();
+      for (VcsRootEntry entry : entries) {
+        Boolean suitable = rootResult.get(entry.getVcsRoot());
+        if (suitable != null) {
+          result.add(suitable);
+        } else {
+          //can be null if the root was broken
+          result.add(false);
+        }
+      }
+      return result;
+    } finally {
+      context.close();
+    }
+  }
+
+
   @Override
   public boolean isAgentSideCheckoutAvailable() {
     return true;
@@ -502,4 +583,9 @@ public class GitVcsSupport extends ServerVcsSupport
   public CommitLoader getCommitLoader() {
     return myCommitLoader;
   }
+
+  @NotNull
+  public RepositoryManager getRepositoryManager() {
+    return myRepositoryManager;
+  }
 }
index b38bccf0783716cba597252467223f1a7338c28d..493face79cc252a26885b558a442f6d92b995882 100644 (file)
@@ -281,7 +281,7 @@ class ModificationDataRevWalk extends RevWalk {
           try {
             prevTreeWalk.setFilter(TreeFilter.ALL);
             prevTreeWalk.setRecursive(true);
-            myContext.addTree(myGitRoot, prevTreeWalk, myRepository, prevRev, true, false);
+            myContext.addTree(myGitRoot, prevTreeWalk, myRepository, prevRev, true, false, null);
             while(prevTreeWalk.next()) {
               String path = prevTreeWalk.getPathString();
               if (path.startsWith(submodulePath + "/")) {
index bf4258d47b81463634a3b78a3092379f0a38081e..2fa97c2624d376a455f22c3edfbb43fb8ac95dbb 100644 (file)
@@ -19,6 +19,7 @@ package jetbrains.buildServer.buildTriggers.vcs.git;
 import com.intellij.openapi.diagnostic.Logger;
 import jetbrains.buildServer.buildTriggers.vcs.git.submodules.SubmoduleResolverImpl;
 import jetbrains.buildServer.util.StringUtil;
+import jetbrains.buildServer.vcs.CheckoutRules;
 import jetbrains.buildServer.vcs.VcsException;
 import jetbrains.buildServer.vcs.VcsRoot;
 import org.eclipse.jgit.errors.NoRemoteRepositoryException;
@@ -59,7 +60,7 @@ public class OperationContext {
 
   public OperationContext(@NotNull final CommitLoader commitLoader,
                           @NotNull final RepositoryManager repositoryManager,
-                          @NotNull final VcsRoot root,
+                          @Nullable final VcsRoot root,
                           @NotNull final String operation,
                           @NotNull final GitProgress progress) {
     myCommitLoader = commitLoader;
@@ -91,6 +92,11 @@ public class OperationContext {
     return result;
   }
 
+  @NotNull
+  public File getRepositoryDir(@NotNull URIish uri) {
+    return myRepositoryManager.getMirrorDir(uri.toString());
+  }
+
   @NotNull
   public Repository getRepositoryFor(@NotNull final URIish uri) throws VcsException {
     File dir = myRepositoryManager.getMirrorDir(uri.toString());
@@ -203,7 +209,7 @@ public class OperationContext {
                       @NotNull Repository db,
                       @NotNull RevCommit commit,
                       boolean ignoreSubmodulesErrors) throws IOException, VcsException {
-    addTree(root, tw, db, commit, ignoreSubmodulesErrors, true);
+    addTree(root, tw, db, commit, ignoreSubmodulesErrors, true, null);
   }
 
   public void addTree(@NotNull GitVcsRoot root,
@@ -211,11 +217,12 @@ public class OperationContext {
                       @NotNull Repository db,
                       @NotNull RevCommit commit,
                       boolean ignoreSubmodulesErrors,
-                      boolean logSubmoduleErrors) throws IOException, VcsException {
+                      boolean logSubmoduleErrors,
+                      @Nullable CheckoutRules rules) throws IOException, VcsException {
     if (root.isCheckoutSubmodules()) {
       SubmoduleResolverImpl submoduleResolver = new SubmoduleResolverImpl(this, myCommitLoader, db, commit, "");
       SubmodulesCheckoutPolicy checkoutPolicy = getPolicyWithErrorsIgnored(root.getSubmodulesCheckoutPolicy(), ignoreSubmodulesErrors);
-      tw.addTree(create(db, commit, submoduleResolver, root.getRepositoryFetchURL().toString(), "", checkoutPolicy, logSubmoduleErrors));
+      tw.addTree(create(db, commit, submoduleResolver, root.getRepositoryFetchURL().toString(), "", checkoutPolicy, logSubmoduleErrors, rules));
     } else {
       tw.addTree(commit.getTree().getId());
     }
@@ -237,4 +244,9 @@ public class OperationContext {
   public GitProgress getProgress() {
     return myProgress;
   }
+
+  @NotNull
+  public RepositoryManager getRepositoryManager() {
+    return myRepositoryManager;
+  }
 }
index 091c77c98df178d52612b23ed32b1910f4a98a3e..9c6a38a539796c679f8a1e4f195ab2d40d62f150 100644 (file)
@@ -54,6 +54,7 @@ import java.lang.management.ManagementFactory;
 import java.lang.management.OperatingSystemMXBean;
 import java.text.ParseException;
 import java.util.*;
+import java.util.concurrent.TimeUnit;
 
 import static com.intellij.openapi.util.text.StringUtil.isEmpty;
 import static com.intellij.openapi.util.text.StringUtil.isEmptyOrSpaces;
@@ -76,6 +77,7 @@ public class PluginConfigImpl implements ServerPluginConfig {
   private static final String MONITORING_FILE_THRESHOLD_SECONDS = "teamcity.git.monitoringFileThresholdSeconds";
   public static final String CREATE_NEW_CONNECTION_FOR_PRUNE = "teamcity.git.newConnectionForPrune";
   public static final String IGNORE_MISSING_REMOTE_REF = "teamcity.git.ignoreMissingRemoteRef";
+  private static final String ACCESS_TIME_UPDATE_RATE_MINUTES = "teamcity.git.accessTimeUpdateRateMinutes";
   private static final String MERGE_RETRY_ATTEMPTS = "teamcity.git.mergeRetryAttemps";
   private static final String GET_REPOSITORY_STATE_TIMEOUT_SECONDS = "teamcity.git.repositoryStateTimeoutSeconds";
   private final static Logger LOG = Logger.getInstance(PluginConfigImpl.class.getName());
@@ -184,11 +186,11 @@ public class PluginConfigImpl implements ServerPluginConfig {
   }
 
   public boolean isRunNativeGC() {
-    return TeamCityProperties.getBoolean("teamcity.server.git.gc.enabled");
+    return TeamCityProperties.getBooleanOrTrue("teamcity.server.git.gc.enabled");
   }
 
   public boolean isRunJGitGC() {
-    return TeamCityProperties.getBoolean("teamcity.git.gcEnabled");
+    return TeamCityProperties.getBoolean("teamcity.git.jgitGcEnabled");
   }
 
   public String getPathToGit() {
@@ -525,6 +527,10 @@ public class PluginConfigImpl implements ServerPluginConfig {
   }
 
   @Override
+  public long getAccessTimeUpdateRateMinutes() {
+    return TeamCityProperties.getLong(ACCESS_TIME_UPDATE_RATE_MINUTES, 5);
+  }
+
   public boolean ignoreMissingRemoteRef() {
     return TeamCityProperties.getBoolean(IGNORE_MISSING_REMOTE_REF);
   }
@@ -533,4 +539,19 @@ public class PluginConfigImpl implements ServerPluginConfig {
   public int getMergeRetryAttempts() {
     return TeamCityProperties.getInteger(MERGE_RETRY_ATTEMPTS, 2);
   }
+
+  @Override
+  public boolean runInPlaceGc() {
+    return TeamCityProperties.getBoolean("teamcity.git.runInPlaceGc");
+  }
+
+  @Override
+  public int getRepackIdleTimeoutSeconds() {
+    return TeamCityProperties.getInteger("teamcity.git.repackIdleTimeoutSeconds", (int) TimeUnit.MINUTES.toSeconds(30));
+  }
+
+  @Override
+  public int getPackRefsIdleTimeoutSeconds() {
+    return TeamCityProperties.getInteger("teamcity.git.packRefsIdleTimeoutSeconds", (int) TimeUnit.MINUTES.toSeconds(5));
+  }
 }
index 665127d9de30fba35fe37fac5915477bae556441..6bf3a3e7749edeb344ab36eb95bd125796fba214 100644 (file)
@@ -47,5 +47,9 @@ public interface RepositoryManager extends MirrorManager {
   @NotNull
   public ReadWriteLock getRmLock(@NotNull File dir);
 
+  <T> T runWithDisabledRemove(@NotNull File dir, @NotNull VcsOperation<T> operation) throws VcsException;
+
+  void runWithDisabledRemove(@NotNull File dir, @NotNull VcsAction action) throws VcsException;
+
   void cleanLocksFor(@NotNull File dir);
 }
index b434613596742ff0bfd70b37f96c834ff6d0ba7d..fbcb47304fe393f0514eaf0b1e9e8d643787bef1 100644 (file)
@@ -24,6 +24,7 @@ import org.eclipse.jgit.lib.RepositoryCache;
 import org.eclipse.jgit.transport.URIish;
 import org.eclipse.jgit.util.FS;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 import java.io.File;
 import java.io.IOException;
@@ -32,6 +33,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -68,6 +70,9 @@ public final class RepositoryManagerImpl implements RepositoryManager {
 
   private final ConcurrentMap<File, Object> myUpdateLastUsedTimeLocks = new ConcurrentHashMap<File, Object>();
 
+  //repo dir -> last access time (nano seconds)
+  private final ConcurrentMap<File, Long> myLastAccessTime = new ConcurrentHashMap<>();
+
   private final AutoCloseRepositoryCache myRepositoryCache = new AutoCloseRepositoryCache();
 
   private final ServerPluginConfig myConfig;
@@ -96,10 +101,17 @@ public final class RepositoryManagerImpl implements RepositoryManager {
   }
 
 
+  @NotNull
   public Map<String, File> getMappings() {
     return myMirrorManager.getMappings();
   }
 
+  @Nullable
+  @Override
+  public String getUrl(@NotNull String cloneDirName) {
+    return myMirrorManager.getUrl(cloneDirName);
+  }
+
   @NotNull
   public List<File> getExpiredDirs() {
     long now = System.currentTimeMillis();
@@ -160,23 +172,24 @@ public final class RepositoryManagerImpl implements RepositoryManager {
 
   @NotNull
   private Repository createRepository(@NotNull final File dir, @NotNull final URIish fetchUrl) throws VcsException {
-    Lock rmLock = getRmLock(dir).readLock();
-    rmLock.lock();
-    try {
+    return runWithDisabledRemove(dir, () -> {
       synchronized (getCreateLock(dir)) {
         Repository result = GitServerUtil.getRepository(dir, fetchUrl);
         return myRepositoryCache.add(RepositoryCache.FileKey.exact(dir, FS.DETECTED), result);
       }
-    } finally {
-      rmLock.unlock();
-    }
+    });
   }
 
 
   private void updateLastUsedTime(@NotNull final File dir) {
+    Long timeNano = myLastAccessTime.get(dir);
+    //don't update last used time too often to decrease file-system activity
+    if (timeNano != null && TimeUnit.NANOSECONDS.toMinutes(System.nanoTime() - timeNano) < myConfig.getAccessTimeUpdateRateMinutes()) {
+      return;
+    }
     Lock rmLock = getRmLock(dir).readLock();
+    rmLock.lock();
     try {
-      rmLock.lock();
       synchronized (getUpdateLastUsedTimeLock(dir)) {
         File timestamp = new File(dir, "timestamp");
         if (!dir.exists() && !dir.mkdirs())
@@ -184,6 +197,7 @@ public final class RepositoryManagerImpl implements RepositoryManager {
         if (!timestamp.exists())
           timestamp.createNewFile();
         FileUtil.writeFileAndReportErrors(timestamp, String.valueOf(System.currentTimeMillis()));
+        myLastAccessTime.put(dir, System.nanoTime());
       }
     } catch (IOException e) {
       LOG.error("Error while updating timestamp in " + dir.getAbsolutePath(), e);
@@ -232,6 +246,29 @@ public final class RepositoryManagerImpl implements RepositoryManager {
   }
 
 
+  @Override
+  public <T> T runWithDisabledRemove(@NotNull File dir, @NotNull VcsOperation<T> operation) throws VcsException {
+    Lock readLock = getRmLock(dir).readLock();
+    readLock.lock();
+    try {
+      return operation.run();
+    } finally {
+      readLock.unlock();
+    }
+  }
+
+
+  @Override
+  public void runWithDisabledRemove(@NotNull File dir, @NotNull VcsAction action) throws VcsException {
+    Lock readLock = getRmLock(dir).readLock();
+    readLock.lock();
+    try {
+      action.run();
+    } finally {
+      readLock.unlock();
+    }
+  }
+
   @NotNull
   public Object getCreateLock(File dir) {
     try {
index c2a00b6bd522d034f23f5bfbf8d3cf5e3115b8f5..a218d2c164f0cf9a7a7579db933e06d7e9b054a8 100644 (file)
@@ -64,13 +64,18 @@ public final class RepositoryRevisionCache {
         return;
       Boolean existing = hasRevision(revision);
       if (existing == null || has != existing) {
-        myCache.put(revision, has);
+        saveRevision(revision, has);
         write();
       }
     }
   }
 
 
+  private void saveRevision(@NotNull String revision, boolean has) throws IOException {
+    myCache.put(revision, has);
+  }
+
+
   void resetNegativeEntries() throws IOException {
     synchronized (myCache) {
       myResetCounter.incrementAndGet();
@@ -97,11 +102,11 @@ public final class RepositoryRevisionCache {
 
       //instead of removing negative entries - turn them into positive, this saves 1 commit lookup
       Set<String> forUpdate = new HashSet<>();
-      for (String commit : myCache.keySet()) {
-        if (newCommits.contains(commit) && Boolean.FALSE.equals(myCache.get(commit)))
+      myCache.forEachEntry((commit, contains) -> {
+        if (newCommits.contains(commit) && Boolean.FALSE.equals(contains))
           forUpdate.add(commit);
-      }
-
+        return true;
+      });
       for (String commit : forUpdate) {
         myCache.put(commit, true);
       }
@@ -134,15 +139,14 @@ public final class RepositoryRevisionCache {
     File cache = getCacheFile(myRepositoryDir, myType);
     cache.getParentFile().mkdirs();
     try (PrintStream printer = new PrintStream(new BufferedOutputStream(new FileOutputStream(cache)))) {
-      for (String revision : myCache.keySet()) {
-        Boolean contains = myCache.get(revision);
+      myCache.forEachEntry((revision, contains) -> {
         if (contains != null) {
           printer.print(contains ? '+' : '-');
           printer.print(revision);
           printer.println();
         }
-      }
-      printer.close();
+        return true;
+      });
     }
   }
 
@@ -193,10 +197,10 @@ public final class RepositoryRevisionCache {
           char c = line.charAt(0);
           switch (c) {
             case '+':
-              result.saveRevision(line.substring(1), true, 0);
+              result.saveRevision(line.substring(1), true);
               break;
             case '-':
-              result.saveRevision(line.substring(1), false, 0);
+              result.saveRevision(line.substring(1), false);
               break;
             default:
               throw new IOException("Bad cache line '" + line + "'");
index 9ea54788a8145df71251f6bf3bacba6865465855..0a88b595e1bb6c5c8a4afc3060eeab4977b30456 100644 (file)
@@ -119,6 +119,6 @@ public final class RevisionsCache {
 
   @NotNull
   private String getRepositoryId(@NotNull File repositoryDir, @NotNull RevisionCacheType type) throws IOException {
-    return repositoryDir.getCanonicalPath() + "_" + type.name();
+    return repositoryDir.getAbsolutePath() + "_" + type.name();
   }
 }
index e8284f461a60a0ad1662a943b27b6a2de7707158..b548f7e471a3af63e01a51425a5321a8b2888d84 100644 (file)
@@ -147,7 +147,15 @@ public interface ServerPluginConfig extends PluginConfig {
 
   boolean createNewConnectionForPrune();
 
+  long getAccessTimeUpdateRateMinutes();
+
   boolean ignoreMissingRemoteRef();
 
   int getMergeRetryAttempts();
+
+  boolean runInPlaceGc();
+
+  int getRepackIdleTimeoutSeconds();
+
+  int getPackRefsIdleTimeoutSeconds();
 }
diff --git a/git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/VcsAction.java b/git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/VcsAction.java
new file mode 100644 (file)
index 0000000..b13e15b
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2000-2017 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;
+
+import jetbrains.buildServer.vcs.VcsException;
+
+public interface VcsAction {
+  void run() throws VcsException;
+}
diff --git a/git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/VcsOperation.java b/git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/VcsOperation.java
new file mode 100644 (file)
index 0000000..9ced32a
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2000-2017 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;
+
+import jetbrains.buildServer.vcs.VcsException;
+
+public interface VcsOperation<T> {
+  T run() throws VcsException;
+}
index eb7abd090974505f1427156fc1b21c0ac769bb7c..55c80632ce1325806c5a8b0897c1bec62c552810 100644 (file)
@@ -43,7 +43,9 @@ public class VcsPropertiesProcessor extends AbstractVcsPropertiesProcessor {
     if (isEmpty(url)) {
       rc.add(new InvalidProperty(Constants.FETCH_URL, "The URL must be specified"));
     } else {
-      if (!mayContainReference(url)) {
+      if (url.contains("\n") || url.contains("\r")) {
+        rc.add(new InvalidProperty(Constants.FETCH_URL, "URL should not contain newline symbols"));
+      } else if (!mayContainReference(url)) {
         try {
           new URIish(url);
         } catch (URISyntaxException e) {
@@ -52,11 +54,15 @@ public class VcsPropertiesProcessor extends AbstractVcsPropertiesProcessor {
       }
     }
     String pushUrl = properties.get(Constants.PUSH_URL);
-    if (!isEmpty(pushUrl) && !mayContainReference(pushUrl)) {
-      try {
-        new URIish(pushUrl);
-      } catch (URISyntaxException e) {
-        rc.add(new InvalidProperty(Constants.PUSH_URL, "Invalid URL syntax: " + pushUrl));
+    if (!isEmpty(pushUrl)) {
+      if (pushUrl.contains("\n") || pushUrl.contains("\r")) {
+        rc.add(new InvalidProperty(Constants.PUSH_URL, "URL should not contain newline symbols"));
+      } else if (!mayContainReference(pushUrl)) {
+        try {
+          new URIish(pushUrl);
+        } catch (URISyntaxException e) {
+          rc.add(new InvalidProperty(Constants.PUSH_URL, "Invalid URL syntax: " + pushUrl));
+        }
       }
     }
 
index 79b68199472bfbe195818904106c79701d12cff9..7887264d72804b3cc7a19782c19ea0bc3e5fc181 100644 (file)
@@ -36,7 +36,6 @@ import java.net.URISyntaxException;
 import java.util.*;
 
 import static jetbrains.buildServer.buildTriggers.vcs.git.GitServerUtil.getAuthorIdent;
-import static jetbrains.buildServer.buildTriggers.vcs.git.GitServerUtil.getFullMessage;
 import static jetbrains.buildServer.buildTriggers.vcs.git.GitServerUtil.getFullUserName;
 import static jetbrains.buildServer.buildTriggers.vcs.git.GitUtils.isTag;
 
@@ -56,16 +55,19 @@ public class GitCommitsInfoBuilder implements CommitsInfoBuilder, GitServerExten
                              @NotNull final CheckoutRules rules,
                              @NotNull final CommitsConsumer consumer) throws VcsException {
     final OperationContext ctx = myVcs.createContext(root, "collecting commits");
-    try {
-      //fetch service is called before, so we may re-use results of it to avoid extra CPU waste
-      final RepositoryStateData currentStateWithTags = myFetchService.getOrCreateRepositoryState(ctx);
-
-      collect(ctx.getRepository(), consumer, currentStateWithTags.getBranchRevisions(), ctx.getGitRoot().isIncludeCommitInfoSubmodules());
-    } catch (Exception e) {
-      throw new VcsException(e);
-    } finally {
-      ctx.close();
-    }
+    GitVcsRoot gitRoot = ctx.getGitRoot();
+    myVcs.getRepositoryManager().runWithDisabledRemove(gitRoot.getRepositoryDir(), () -> {
+      try {
+        //fetch service is called before, so we may re-use results of it to avoid extra CPU waste
+        final RepositoryStateData currentStateWithTags = myFetchService.getOrCreateRepositoryState(ctx);
+
+        collect(ctx.getRepository(), consumer, currentStateWithTags.getBranchRevisions(), gitRoot.isIncludeCommitInfoSubmodules());
+      } catch (Exception e) {
+        throw new VcsException(e);
+      } finally {
+        ctx.close();
+      }
+    });
   }
 
   private void collect(@NotNull final Repository db,
index 59da4997d2470630621df572c1b60c1a7b96bd59..8d301fe8e77d1d309edbc39a14d486627669dabd 100644 (file)
 
 package jetbrains.buildServer.buildTriggers.vcs.git.patch;
 
-import jetbrains.buildServer.buildTriggers.vcs.git.GitServerExtension;
-import jetbrains.buildServer.buildTriggers.vcs.git.GitVcsSupport;
-import jetbrains.buildServer.buildTriggers.vcs.git.OperationContext;
-import jetbrains.buildServer.buildTriggers.vcs.git.ServerPluginConfig;
+import jetbrains.buildServer.buildTriggers.vcs.git.*;
 import jetbrains.buildServer.vcs.BulkPatchService;
 import jetbrains.buildServer.vcs.CheckoutRules;
 import jetbrains.buildServer.vcs.VcsException;
@@ -54,48 +51,51 @@ public class BulkPatchBuilderImpl implements BulkPatchService, GitServerExtensio
                            @NotNull final List<BulkPatchBuilderRequest> requests,
                            @NotNull final BulkPatchBuilder patch) throws VcsException, IOException {
     final OperationContext ctx = myVcs.createContext(root, "bulk patch " + requests.size() + " commits");
-    try {
-      final Repository myRepo = ctx.getRepository();
-      final ObjectReader contentsReader = myRepo.getObjectDatabase().newReader();
-      final ObjectReader treesReader = myRepo.getObjectDatabase().newReader();
+    GitVcsRoot gitRoot = ctx.getGitRoot();
+    myVcs.getRepositoryManager().runWithDisabledRemove(gitRoot.getRepositoryDir(), () -> {
+      try {
+        final Repository myRepo = ctx.getRepository();
+        final ObjectReader contentsReader = myRepo.getObjectDatabase().newReader();
+        final ObjectReader treesReader = myRepo.getObjectDatabase().newReader();
 
-      for (BulkPatchBuilderRequest request : requests) {
-        final PatchBuilder patchBuilder = patch.startPatch(request);
+        for (BulkPatchBuilderRequest request : requests) {
+          final PatchBuilder patchBuilder = patch.startPatch(request);
 
-        final String prevBase = request.getFromVersion();
-        final String toBase = request.getToVersion();
+          final String prevBase = request.getFromVersion();
+          final String toBase = request.getToVersion();
 
-        try {
-          new GitPatchBuilder(ctx, patchBuilder, prevBase, toBase, rules, myConfig.verboseTreeWalkLog()) {
-            @NotNull
-            @Override
-            protected ObjectReader newObjectReaderForTree() {
-              return treesReader;
-            }
+          try {
+            new GitPatchBuilder(ctx, patchBuilder, prevBase, toBase, rules, myConfig.verboseTreeWalkLog()) {
+              @NotNull
+              @Override
+              protected ObjectReader newObjectReaderForTree() {
+                return treesReader;
+              }
 
-            @NotNull
-            @Override
-            protected ContentLoaderFactory contentLoaderFactory() {
-              return new ContentLoaderFactory() {
-                @Nullable
-                public ObjectLoader open(@NotNull final Repository repo, @NotNull final ObjectId id) throws IOException {
-                  assert repo == myRepo;
-                  return contentsReader.open(id);
-                }
-              };
-            }
-          }.buildPatch();
+              @NotNull
+              @Override
+              protected ContentLoaderFactory contentLoaderFactory() {
+                return new ContentLoaderFactory() {
+                  @Nullable
+                  public ObjectLoader open(@NotNull final Repository repo, @NotNull final ObjectId id) throws IOException {
+                    assert repo == myRepo;
+                    return contentsReader.open(id);
+                  }
+                };
+              }
+            }.buildPatch();
 
-        } catch (Throwable e) {
-          throw new VcsException("Failed to build patch " + prevBase + " -> " + toBase + ". " + e.getMessage(), e);
-        } finally {
-          patch.endPatch(request, patchBuilder);
+          } catch (Throwable e) {
+            throw new VcsException("Failed to build patch " + prevBase + " -> " + toBase + ". " + e.getMessage(), e);
+          } finally {
+            patch.endPatch(request, patchBuilder);
+          }
         }
+      } catch (Throwable e) {
+        throw new VcsException("Failed to complete bulk patch." + e.getMessage(), e);
+      } finally {
+        ctx.close();
       }
-    } catch (Throwable e) {
-      throw new VcsException("Failed to complete bulk patch." + e.getMessage(), e);
-    } finally {
-      ctx.close();
-    }
+    });
   }
 }
index b4c56c7833d1ba365c674a6e499710a944feadd5..83e655e8024cc63d66baeb2f9605b9144852d075 100644 (file)
@@ -107,7 +107,7 @@ public class GitPatchBuilder {
     RevCommit toCommit = myContext.findCommit(myRepository, myToRevision);
     if (toCommit == null)
       throw new VcsException("Cannot find commit " + myToRevision + " in repository " + myRepository.getDirectory().getAbsolutePath());
-    myContext.addTree(myGitRoot, myTreeWalk, myRepository, toCommit, false);
+    myContext.addTree(myGitRoot, myTreeWalk, myRepository, toCommit, false, true, myRules);
   }
 
   private void addFromCommitTree() throws IOException, VcsException {
@@ -123,7 +123,7 @@ public class GitPatchBuilder {
         myTreeWalk.addTree(new EmptyTreeIterator());
         myFullCheckout = true;
       } else {
-        myContext.addTree(myGitRoot, myTreeWalk, myRepository, fromCommit, true);
+        myContext.addTree(myGitRoot, myTreeWalk, myRepository, fromCommit, true, true, myRules);
       }
     }
   }
index 7dd6b1a89e25335e08babc74c6504ad2b6b87d97..9fd53e39c9e08946964d7e9af2f0e1b0f4c81e40 100755 (executable)
@@ -19,6 +19,8 @@ package jetbrains.buildServer.buildTriggers.vcs.git.submodules;
 import com.intellij.openapi.diagnostic.Logger;
 import jetbrains.buildServer.buildTriggers.vcs.git.SubmodulesCheckoutPolicy;
 import jetbrains.buildServer.buildTriggers.vcs.git.VcsAuthenticationException;
+import jetbrains.buildServer.vcs.CheckoutRules;
+import jetbrains.buildServer.vcs.IncludeRule;
 import jetbrains.buildServer.vcs.VcsException;
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.errors.TransportException;
@@ -49,7 +51,7 @@ public abstract class SubmoduleAwareTreeIterator extends AbstractTreeIterator {
    */
   private final String myUrl;
   /**
-   * Path from root of the main repository to the entry of repository of this iterator, used in error messages.
+   * Path from root of the main repository to the entry of repository of this iterator.
    * For main repository it is equals "", for repository of submodule it is equals to submodule path,
    * for sub-submodule path of parent submodule + path of current submodule and so on.
    */
@@ -92,6 +94,9 @@ public abstract class SubmoduleAwareTreeIterator extends AbstractTreeIterator {
 
   private final boolean myLogSubmoduleErrors;
 
+  //submodules excluded by the these rules will not be resolved; null means resolve all submodules
+  private CheckoutRules myRules;
+
   /**
    * The constructor
    *
@@ -148,6 +153,11 @@ public abstract class SubmoduleAwareTreeIterator extends AbstractTreeIterator {
     movedToEntry();
   }
 
+
+  void setCheckoutRules(CheckoutRules rules) {
+    myRules = rules;
+  }
+
   /**
    * @return the current repository for the submodule
    */
@@ -167,10 +177,29 @@ public abstract class SubmoduleAwareTreeIterator extends AbstractTreeIterator {
       return;
     }
     int wrappedMode = myWrappedIterator.getEntryRawMode();
+    String entryPath = myWrappedIterator.getEntryPathString();
     myIsOnSubmodule = checkoutSubmodules() && GITLINK_MODE_BITS == wrappedMode;
+    if (myIsOnSubmodule && myRules != null) {
+      //if submodule dir is excluded by checkout rules we can treat it as an empty dir
+      String pathFromRoot = getPathFromRoot(entryPath);
+      if (myRules.map(pathFromRoot) == null) {
+        //submodule dir itself is excluded, but some of its dirs can be included
+        //happens with checkout rules like +:submodule/dir1
+        boolean rulesInsideSubmodule = false;
+        CheckoutRules submoduleAsRule = new CheckoutRules("+:" + pathFromRoot);
+        for (IncludeRule rule : myRules.getRootIncludeRules()) {
+          if (submoduleAsRule.map(rule.getFrom()) != null) {
+            rulesInsideSubmodule = true;
+            break;
+          }
+        }
+        if (!rulesInsideSubmodule) {
+          myIsOnSubmodule = false;
+        }
+      }
+    }
     mode = myIsOnSubmodule ? TREE_MODE_BITS : wrappedMode;
     if (myIsOnSubmodule) {
-      String entryPath = myWrappedIterator.getEntryPathString();
       try {
         mySubmoduleCommit = getSubmoduleCommit(entryPath, myWrappedIterator.getEntryObjectId());
       } catch (Exception e) {
@@ -307,7 +336,8 @@ public abstract class SubmoduleAwareTreeIterator extends AbstractTreeIterator {
                                               mySubmoduleResolver.getSubmoduleUrl(path),
                                               getPathFromRoot(path),
                                               SubmodulesCheckoutPolicy.getSubSubModulePolicyFor(mySubmodulesPolicy),
-                                              myLogSubmoduleErrors);
+                                              myLogSubmoduleErrors,
+                                              myRules);
     } else {
       Repository r = mySubmoduleResolver.getRepository();
       ObjectReader or = r.newObjectReader();
@@ -324,7 +354,8 @@ public abstract class SubmoduleAwareTreeIterator extends AbstractTreeIterator {
                                               myUrl,
                                               myPathFromRoot,
                                               mySubmodulesPolicy,
-                                              myLogSubmoduleErrors);
+                                              myLogSubmoduleErrors,
+                                              myRules);
     }
   }
 
index 415a2d9309505f11a264cba66b444e78839e1e52..8f50cb75f70275ab02dcc45fea9591e59e61d6e1 100644 (file)
@@ -18,6 +18,7 @@ package jetbrains.buildServer.buildTriggers.vcs.git.submodules;
 
 import com.intellij.util.containers.IntArrayList;
 import jetbrains.buildServer.buildTriggers.vcs.git.SubmodulesCheckoutPolicy;
+import jetbrains.buildServer.vcs.CheckoutRules;
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
@@ -49,9 +50,10 @@ public class SubmoduleAwareTreeIteratorFactory {
                                                   String repositoryUrl,
                                                   String pathFromRoot,
                                                   SubmodulesCheckoutPolicy submodulePolicy,
-                                                  boolean logSubmoduleErrors)
+                                                  boolean logSubmoduleErrors,
+                                                  CheckoutRules rules)
     throws IOException {
-    return createSubmoduleAwareTreeIterator(null, createTreeParser(db, commit), subResolver, "", repositoryUrl, pathFromRoot, submodulePolicy, logSubmoduleErrors);
+    return createSubmoduleAwareTreeIterator(null, createTreeParser(db, commit), subResolver, "", repositoryUrl, pathFromRoot, submodulePolicy, logSubmoduleErrors, rules);
   }
 
 
@@ -75,25 +77,30 @@ public class SubmoduleAwareTreeIteratorFactory {
                                                                              String repositoryUrl,
                                                                              String pathFromRoot,
                                                                              SubmodulesCheckoutPolicy submodulesPolicy,
-                                                                             boolean logSubmoduleErrors) throws IOException {
+                                                                             boolean logSubmoduleErrors,
+                                                                             CheckoutRules rules) throws IOException {
+    SubmoduleAwareTreeIterator result;
     if (subResolver.containsSubmodule(path)) {
       int[] mapping = buildMapping(wrapped);
       String submoduleUrl = subResolver.getSubmoduleUrl(path);
       if (submoduleUrl == null)
         submoduleUrl = repositoryUrl;
       if (mapping == null) {
-        return parent == null
+        result = parent == null
                ? new DirectSubmoduleAwareTreeIterator(wrapped, subResolver, submoduleUrl, pathFromRoot, submodulesPolicy, logSubmoduleErrors)
                : new DirectSubmoduleAwareTreeIterator(parent, wrapped, subResolver, submoduleUrl, pathFromRoot, submodulesPolicy, logSubmoduleErrors);
       } else {
-        return parent == null
+        result = parent == null
                ? new IndirectSubmoduleAwareTreeIterator(wrapped, subResolver, mapping, submoduleUrl, pathFromRoot, submodulesPolicy, logSubmoduleErrors)
                : new IndirectSubmoduleAwareTreeIterator(parent, wrapped, subResolver, mapping, submoduleUrl, pathFromRoot, submodulesPolicy, logSubmoduleErrors);
       }
-    }
-    return parent == null
+    } else {
+      result = parent == null
            ? new DirectSubmoduleAwareTreeIterator(wrapped, subResolver, repositoryUrl, pathFromRoot, submodulesPolicy, logSubmoduleErrors)
            : new DirectSubmoduleAwareTreeIterator(parent, wrapped, subResolver, repositoryUrl, pathFromRoot, submodulesPolicy, logSubmoduleErrors);
+    }
+    result.setCheckoutRules(rules);
+    return result;
   }
 
 
index 4fcb2d37b8e051c97db3fc35dc9a5390ad3af8c0..cf2ac4338832f97e4e5bef9121cf7f7daead31e4 100755 (executable)
@@ -33,6 +33,7 @@ import org.eclipse.jgit.transport.RefSpec;
 import org.eclipse.jgit.transport.URIish;
 import org.jetbrains.annotations.NotNull;
 
+import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.net.URISyntaxException;
@@ -92,20 +93,42 @@ public class SubmoduleResolverImpl implements SubmoduleResolver {
     if (submodule == null)
       throw new MissingSubmoduleEntryException(parentRepositoryUrl, myCommit.name(), path);
 
-    Repository r = resolveRepository(submodule.getUrl());
-    String submoduleUrl = myContext.getConfig(r).getString("teamcity", null, "remote");
+    URIish submoduleUri = resolveSubmoduleUrl(submodule.getUrl());
+    File repositoryDir = myContext.getRepositoryDir(submoduleUri);
+    try {
+      return myContext.getRepositoryManager().runWithDisabledRemove(repositoryDir, () -> {
+        try {
+          Repository r = resolveRepository(submodule.getUrl());
+          String submoduleUrl = myContext.getConfig(r).getString("teamcity", null, "remote");
 
-    if (!isCommitExist(r, commit)) {
-      try {
-        fetch(r, path, submodule.getUrl());
-      } catch (Exception e) {
-        throw new SubmoduleFetchException(parentRepositoryUrl, path, submoduleUrl, myCommit, e);
+          if (!isCommitExist(r, commit)) {
+            try {
+              fetch(r, path, submodule.getUrl());
+            } catch (Exception e) {
+              throw new SubmoduleFetchException(parentRepositoryUrl, path, submoduleUrl, myCommit, e);
+            }
+          }
+          try {
+            return myCommitLoader.getCommit(r, commit);
+          } catch (Exception e) {
+            throw new MissingSubmoduleCommitException(parentRepositoryUrl, myCommit.name(), path, submodule.getUrl(), commit.name());
+          }
+        } catch (Exception e) {
+          throw new RuntimeException(e);
+        }
+      });
+    } catch (RuntimeException e) {
+      Throwable cause = e.getCause();
+      if (cause instanceof CorruptObjectException) {
+        throw (CorruptObjectException) cause;
       }
-    }
-    try {
-      return myCommitLoader.getCommit(r, commit);
-    } catch (Exception e) {
-      throw new MissingSubmoduleCommitException(parentRepositoryUrl, myCommit.name(), path, submodule.getUrl(), commit.name());
+      if (cause instanceof VcsException) {
+        throw (VcsException) cause;
+      }
+      if (cause instanceof URISyntaxException) {
+        throw (URISyntaxException) cause;
+      }
+      throw new VcsException(e);
     }
   }
 
diff --git a/git-tests/data/excluded_broken_submodule/after/dir/b.txt b/git-tests/data/excluded_broken_submodule/after/dir/b.txt
new file mode 100755 (executable)
index 0000000..ce8fc27
--- /dev/null
@@ -0,0 +1,8 @@
+b line 1\r
+b line 2\r
+b line 3\r
+b line 4\r
+b line 5\r
+b line 6\r
+b line 7\r
+b line 8 (new)\r
diff --git a/git-tests/data/excluded_broken_submodule/after/dir/d.txt b/git-tests/data/excluded_broken_submodule/after/dir/d.txt
new file mode 100755 (executable)
index 0000000..baefcc5
--- /dev/null
@@ -0,0 +1,7 @@
+d line 1\r
+d line 2\r
+d line 3\r
+d line 4\r
+d line 5\r
+d line 6\r
+d line 7\r
diff --git a/git-tests/data/excluded_broken_submodule/after/dir/q.txt b/git-tests/data/excluded_broken_submodule/after/dir/q.txt
new file mode 100755 (executable)
index 0000000..823f52f
--- /dev/null
@@ -0,0 +1,6 @@
+q line 1\r
+q line 1\r
+q line 1\r
+q line 1\r
+q line 1\r
+q line 1
\ No newline at end of file
diff --git a/git-tests/data/submodules_and_checkout_rules/after/first-level-submodule/.gitmodules b/git-tests/data/submodules_and_checkout_rules/after/first-level-submodule/.gitmodules
new file mode 100644 (file)
index 0000000..30c9907
--- /dev/null
@@ -0,0 +1,3 @@
+[submodule "sub-submodule"]
+       path = sub-sub
+       url = ../submodule.git
diff --git a/git-tests/data/submodules_and_checkout_rules/after/first-level-submodule/sub-sub/file.txt b/git-tests/data/submodules_and_checkout_rules/after/first-level-submodule/sub-sub/file.txt
new file mode 100644 (file)
index 0000000..ad05627
--- /dev/null
@@ -0,0 +1 @@
+The sample file is submodule.
\ No newline at end of file
diff --git a/git-tests/data/submodules_and_checkout_rules/after/first-level-submodule/sub-sub/new file.txt b/git-tests/data/submodules_and_checkout_rules/after/first-level-submodule/sub-sub/new file.txt
new file mode 100644 (file)
index 0000000..e56de42
--- /dev/null
@@ -0,0 +1 @@
+This is a new file in submodule.
\ No newline at end of file
diff --git a/git-tests/data/submodules_and_checkout_rules/after/first-level-submodule/submoduleFile.txt b/git-tests/data/submodules_and_checkout_rules/after/first-level-submodule/submoduleFile.txt
new file mode 100644 (file)
index 0000000..b2adbe6
--- /dev/null
@@ -0,0 +1 @@
+this is submodule that has another submodule
diff --git a/git-tests/data/submodules_and_checkout_rules2/after/first-level-submodule/sub-sub/file.txt b/git-tests/data/submodules_and_checkout_rules2/after/first-level-submodule/sub-sub/file.txt
new file mode 100644 (file)
index 0000000..ad05627
--- /dev/null
@@ -0,0 +1 @@
+The sample file is submodule.
\ No newline at end of file
diff --git a/git-tests/data/submodules_and_checkout_rules2/after/first-level-submodule/sub-sub/new file.txt b/git-tests/data/submodules_and_checkout_rules2/after/first-level-submodule/sub-sub/new file.txt
new file mode 100644 (file)
index 0000000..e56de42
--- /dev/null
@@ -0,0 +1 @@
+This is a new file in submodule.
\ No newline at end of file
diff --git a/git-tests/data/submodules_and_checkout_rules3/after/.gitmodules b/git-tests/data/submodules_and_checkout_rules3/after/.gitmodules
new file mode 100644 (file)
index 0000000..6f4e9aa
--- /dev/null
@@ -0,0 +1,3 @@
+[submodule "first-level-submodule"]
+       path = first-level-submodule
+       url = ../sub-submodule.git
diff --git a/git-tests/data/submodules_and_checkout_rules3/after/dir/b.txt b/git-tests/data/submodules_and_checkout_rules3/after/dir/b.txt
new file mode 100755 (executable)
index 0000000..ce8fc27
--- /dev/null
@@ -0,0 +1,8 @@
+b line 1\r
+b line 2\r
+b line 3\r
+b line 4\r
+b line 5\r
+b line 6\r
+b line 7\r
+b line 8 (new)\r
diff --git a/git-tests/data/submodules_and_checkout_rules3/after/dir/d.txt b/git-tests/data/submodules_and_checkout_rules3/after/dir/d.txt
new file mode 100755 (executable)
index 0000000..baefcc5
--- /dev/null
@@ -0,0 +1,7 @@
+d line 1\r
+d line 2\r
+d line 3\r
+d line 4\r
+d line 5\r
+d line 6\r
+d line 7\r
diff --git a/git-tests/data/submodules_and_checkout_rules3/after/dir/q.txt b/git-tests/data/submodules_and_checkout_rules3/after/dir/q.txt
new file mode 100755 (executable)
index 0000000..823f52f
--- /dev/null
@@ -0,0 +1,6 @@
+q line 1\r
+q line 1\r
+q line 1\r
+q line 1\r
+q line 1\r
+q line 1
\ No newline at end of file
diff --git a/git-tests/data/submodules_and_checkout_rules3/after/first-level-submodule/.gitmodules b/git-tests/data/submodules_and_checkout_rules3/after/first-level-submodule/.gitmodules
new file mode 100644 (file)
index 0000000..30c9907
--- /dev/null
@@ -0,0 +1,3 @@
+[submodule "sub-submodule"]
+       path = sub-sub
+       url = ../submodule.git
diff --git a/git-tests/data/submodules_and_checkout_rules3/after/first-level-submodule/sub-sub/new file.txt b/git-tests/data/submodules_and_checkout_rules3/after/first-level-submodule/sub-sub/new file.txt
new file mode 100644 (file)
index 0000000..e56de42
--- /dev/null
@@ -0,0 +1 @@
+This is a new file in submodule.
\ No newline at end of file
diff --git a/git-tests/data/submodules_and_checkout_rules3/after/first-level-submodule/submoduleFile.txt b/git-tests/data/submodules_and_checkout_rules3/after/first-level-submodule/submoduleFile.txt
new file mode 100644 (file)
index 0000000..b2adbe6
--- /dev/null
@@ -0,0 +1 @@
+this is submodule that has another submodule
diff --git a/git-tests/data/submodules_and_checkout_rules3/after/readme.txt b/git-tests/data/submodules_and_checkout_rules3/after/readme.txt
new file mode 100755 (executable)
index 0000000..ebfc861
--- /dev/null
@@ -0,0 +1 @@
+Test repository for teamcity.
\ No newline at end of file
diff --git a/git-tests/data/submodules_and_checkout_rules4/after/first-level-submodule/sub-sub/file.txt b/git-tests/data/submodules_and_checkout_rules4/after/first-level-submodule/sub-sub/file.txt
new file mode 100644 (file)
index 0000000..ad05627
--- /dev/null
@@ -0,0 +1 @@
+The sample file is submodule.
\ No newline at end of file
index 4fef1251963c2396f462447dba2fd45f65f62507..8b3414a5857e44de2f85882596e2bf1e32bb1703 100644 (file)
@@ -24,7 +24,7 @@
     <orderEntry type="library" name="TeamCity Vcs Api" level="project" />
     <orderEntry type="module" module-name="git-server-tc" />
     <orderEntry type="library" name="quartz-1.6.0" level="project" />
-    <orderEntry type="library" name="httpclient-4.3.4" level="project" />
+    <orderEntry type="library" name="httpclient" level="project" />
     <orderEntry type="library" name="JavaEWAH-0.7.9" level="project" />
   </component>
 </module>
\ No newline at end of file
index 34cb8ea21a399baf75d220a04d2514b994baa90c..80f4a58ff250c55800c0fee896d04dc7e8b3093f 100644 (file)
@@ -17,7 +17,6 @@
 package jetbrains.buildServer.buildTriggers.vcs.git.tests;
 
 import com.intellij.openapi.util.SystemInfo;
-import com.intellij.openapi.util.io.FileUtil;
 import com.intellij.openapi.util.io.StreamUtil;
 import jetbrains.buildServer.TempFiles;
 import jetbrains.buildServer.TestInternalProperties;
@@ -33,6 +32,7 @@ import jetbrains.buildServer.buildTriggers.vcs.git.agent.command.UpdateRefComman
 import jetbrains.buildServer.buildTriggers.vcs.git.agent.command.impl.*;
 import jetbrains.buildServer.buildTriggers.vcs.git.agent.errors.GitExecTimeout;
 import jetbrains.buildServer.ssh.VcsRootSshKeyManager;
+import jetbrains.buildServer.util.FileUtil;
 import jetbrains.buildServer.util.TestFor;
 import jetbrains.buildServer.vcs.CheckoutRules;
 import jetbrains.buildServer.vcs.VcsException;
@@ -601,7 +601,7 @@ public class AgentVcsSupportTest {
     //create ref pointing to invalid object
     File gitDir = new File(myCheckoutDir, ".git");
     String invalidObject = "bba7fbcc200b4968e6abd2f7d475dc15306cafc1";
-    jetbrains.buildServer.util.FileUtil.writeFile(new File(gitDir, "refs/heads/brokenRef"), invalidObject);
+    FileUtil.writeFile(new File(gitDir, "refs/heads/brokenRef"), invalidObject);
 
     //update remote repo
     delete(remoteRepo);
@@ -910,6 +910,17 @@ public class AgentVcsSupportTest {
   }
 
 
+  @TestFor(issues = "TW-50714")
+  @Test(dataProvider = "mirrors")
+  public void fetch_all_heads__non_head_ref(boolean useMirrors) throws Exception {
+    AgentRunningBuild build = createRunningBuild(map(PluginConfigImpl.FETCH_ALL_HEADS, "true"));
+
+    myRoot = vcsRoot().withAgentGitPath(getGitPath()).withFetchUrl(GitUtils.toURL(myMainRepo)).withUseMirrors(useMirrors).withBranch("refs/pull/1").build();
+
+    myVcsSupport.updateSources(myRoot, CheckoutRules.DEFAULT, "b896070465af79121c9a4eb5300ecff29453c164", myCheckoutDir, build, false);
+  }
+
+
   private void removeTag(@NotNull File dotGitDir, @NotNull String tagName) {
     delete(tagFile(dotGitDir, tagName));
   }
@@ -1092,7 +1103,33 @@ public class AgentVcsSupportTest {
     if (switchBranch)
       myBuild = createRunningBuild(map(GitUtils.getGitRootBranchParamName(myRoot), "refs/heads/personal-branch1"));
     myVcsSupport.updateSources(myRoot, CheckoutRules.DEFAULT, "ad4528ed5c84092fdbe9e0502163cf8d6e6141e7", myCheckoutDir, myBuild, false);
-    then(jetbrains.buildServer.util.FileUtil.readFile(f)).doesNotContain("update by build script");
+    then(FileUtil.readFile(f)).doesNotContain("update by build script");
+  }
+
+
+  @TestFor(issues = "TW-40313")
+  public void should_remove_orphaned_indexes() throws Exception {
+    //checkout
+    VcsRootImpl root = vcsRoot()
+      .withAgentGitPath(getGitPath())
+      .withFetchUrl(GitUtils.toURL(myMainRepo))
+      .build();
+
+    myVcsSupport.updateSources(root, CheckoutRules.DEFAULT, "465ad9f630e451b9f2b782ffb09804c6a98c4bb9", myCheckoutDir, createRunningBuild(true), false);
+
+    //create orphaned idx files
+    File mirror = myBuilder.getMirrorManager().getMirrorDir(GitUtils.toURL(myMainRepo));
+    File idxInMirror = new File(new File(new File(mirror, "objects"), "pack"), "whatever.idx");
+    FileUtil.writeFileAndReportErrors(idxInMirror, "whatever");
+    File idxInCheckoutDir = new File(new File(new File(mirror, "objects"), "pack"), "whatever.idx");
+    FileUtil.writeFileAndReportErrors(idxInCheckoutDir, "whatever");
+
+    //checkout again
+    myVcsSupport.updateSources(root, CheckoutRules.DEFAULT, "465ad9f630e451b9f2b782ffb09804c6a98c4bb9", myCheckoutDir, createRunningBuild(true), false);
+
+    //orphaned idx files are removed
+    then(idxInCheckoutDir).doesNotExist();
+    then(idxInMirror).doesNotExist();
   }
 
 
index 0905f562039b1732a87ebf772ab50336cb601595..229d0b30b85420d25be8a9d3c3bc4984ec43a855 100644 (file)
@@ -24,17 +24,20 @@ import jetbrains.buildServer.buildTriggers.vcs.git.AuthenticationMethod;
 import jetbrains.buildServer.buildTriggers.vcs.git.Constants;
 import jetbrains.buildServer.buildTriggers.vcs.git.agent.*;
 import jetbrains.buildServer.util.FileUtil;
+import jetbrains.buildServer.util.TestFor;
 import jetbrains.buildServer.vcs.CheckoutRules;
 import jetbrains.buildServer.vcs.VcsException;
 import jetbrains.buildServer.vcs.VcsRoot;
 import org.jetbrains.annotations.NotNull;
 import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
 import java.io.File;
 import java.io.IOException;
 
 import static jetbrains.buildServer.buildTriggers.vcs.git.agent.UpdaterImpl.GIT_WITH_SPARSE_CHECKOUT;
+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 org.assertj.core.api.BDDAssertions.then;
@@ -60,16 +63,16 @@ public class AutoCheckoutTest extends BaseRemoteRepositoryTest {
   public void git_client_found_by_path_from_root() throws IOException, VcsException {
     myVcsSupport = vcsSupportWithRealGit();
 
-    VcsRoot vcsRoot = vcsRootWithAgentGitPath("git");
+    VcsRoot vcsRoot = vcsRootWithAgentGitPath();
 
-    verifyCanCheckout(vcsRoot, CheckoutRules.DEFAULT, runningBuild().build());
+    verifyCanCheckout(vcsRoot, CheckoutRules.DEFAULT, runningBuild().addRoot(vcsRoot).build());
   }
 
   public void client_found_by_path_from_environment() throws IOException, VcsException {
     myVcsSupport = vcsSupportWithRealGit();
 
     VcsRoot vcsRoot = vcsRootWithAgentGitPath(null);
-    AgentRunningBuild build = runningBuild().sharedEnvVariable(Constants.TEAMCITY_AGENT_GIT_PATH, "git").build();
+    AgentRunningBuild build = runningBuild().sharedEnvVariable(Constants.TEAMCITY_AGENT_GIT_PATH, getGitPath()).addRoot(vcsRoot).build();
 
     verifyCanCheckout(vcsRoot, CheckoutRules.DEFAULT, build);
   }
@@ -79,7 +82,7 @@ public class AutoCheckoutTest extends BaseRemoteRepositoryTest {
 
     VcsRoot vcsRoot =  vcsRootWithAgentGitPath("gitt");
 
-    AgentCheckoutAbility canCheckout = myVcsSupport.canCheckout(vcsRoot, CheckoutRules.DEFAULT, runningBuild().build());
+    AgentCheckoutAbility canCheckout = myVcsSupport.canCheckout(vcsRoot, CheckoutRules.DEFAULT, runningBuild().addRoot(vcsRoot).build());
     then(canCheckout.getCanNotCheckoutReason().getType()).isEqualTo(AgentCanNotCheckoutReason.NO_VCS_CLIENT);
     then(canCheckout.getCanNotCheckoutReason().getDetails()).contains("Unable to run git at path gitt");
   }
@@ -87,34 +90,49 @@ public class AutoCheckoutTest extends BaseRemoteRepositoryTest {
   public void exclude_rules_are_used_without_sparse_checkout() throws IOException, VcsException {
     myVcsSupport = vcsSupportWithFakeGitOfVersion(GIT_WITH_SPARSE_CHECKOUT);
 
-    VcsRoot vcsRoot = vcsRootWithAgentGitPath("git");
-    AgentRunningBuild build = runningBuild().sharedConfigParams(PluginConfigImpl.USE_SPARSE_CHECKOUT, "false").build();
+    VcsRoot vcsRoot = vcsRootWithAgentGitPath();
+    AgentRunningBuild build = runningBuild().sharedConfigParams(PluginConfigImpl.USE_SPARSE_CHECKOUT, "false")
+      .addRootEntry(vcsRoot, "-:dir/q.txt").build();
 
     AgentCheckoutAbility canCheckout = myVcsSupport.canCheckout(vcsRoot, new CheckoutRules("-:dir/q.txt"), build);
     then(canCheckout.getCanNotCheckoutReason().getType()).isEqualTo(AgentCanNotCheckoutReason.NOT_SUPPORTED_CHECKOUT_RULES);
-    then(canCheckout.getCanNotCheckoutReason().getDetails()).contains("Exclude rules are not supported for agent checkout");
+    then(canCheckout.getCanNotCheckoutReason().getDetails()).contains("Cannot perform sparse checkout using git " + GIT_WITH_SPARSE_CHECKOUT);
   }
 
   public void include_rule_with_mapping_is_used_without_sparse_checkout() throws IOException, VcsException {
     myVcsSupport =  vcsSupportWithFakeGitOfVersion(GIT_WITH_SPARSE_CHECKOUT);
 
-    VcsRoot vcsRoot = vcsRootWithAgentGitPath("git");
-    AgentRunningBuild build = runningBuild().sharedConfigParams(PluginConfigImpl.USE_SPARSE_CHECKOUT, "false").build();
+    VcsRoot vcsRoot = vcsRootWithAgentGitPath();
+    AgentRunningBuild build = runningBuild().sharedConfigParams(PluginConfigImpl.USE_SPARSE_CHECKOUT, "false")
+      .addRootEntry(vcsRoot, "+:a/b/c => d").build();
 
     AgentCheckoutAbility canCheckout = myVcsSupport.canCheckout(vcsRoot, new CheckoutRules("+:a/b/c => d"), build);
     then(canCheckout.getCanNotCheckoutReason().getType()).isEqualTo(AgentCanNotCheckoutReason.NOT_SUPPORTED_CHECKOUT_RULES);
-    then(canCheckout.getCanNotCheckoutReason().getDetails()).contains("Agent checkout for the git supports only include rule of form '. => subdir'");
+    then(canCheckout.getCanNotCheckoutReason().getDetails()).contains("Unsupported rules for agent-side checkout: +:a/b/c => d");
   }
 
   public void git_version_does_not_support_sparse_checkout() throws IOException, VcsException {
-    myVcsSupport =  vcsSupportWithFakeGitOfVersion(GIT_WITH_SPARSE_CHECKOUT.previousVersion());
+    GitVersion gitVersion = GIT_WITH_SPARSE_CHECKOUT.previousVersion();
+    myVcsSupport =  vcsSupportWithFakeGitOfVersion(gitVersion);
 
-    VcsRoot vcsRoot = vcsRootWithAgentGitPath("git");
-    AgentRunningBuild build = runningBuild().sharedConfigParams(PluginConfigImpl.USE_SPARSE_CHECKOUT, "true").build();
+    VcsRoot vcsRoot = vcsRootWithAgentGitPath(getGitPath());
+    AgentRunningBuild build = runningBuild().sharedConfigParams(PluginConfigImpl.USE_SPARSE_CHECKOUT, "true")
+      .addRootEntry(vcsRoot, "-:dir/q.txt").build();
 
     AgentCheckoutAbility canCheckout = myVcsSupport.canCheckout(vcsRoot, new CheckoutRules("-:dir/q.txt"), build);
     then(canCheckout.getCanNotCheckoutReason().getType()).isEqualTo(AgentCanNotCheckoutReason.NOT_SUPPORTED_CHECKOUT_RULES);
-    then(canCheckout.getCanNotCheckoutReason().getDetails()).contains("Exclude rules are not supported for agent checkout");
+    then(canCheckout.getCanNotCheckoutReason().getDetails()).contains("Cannot perform sparse checkout using git " + gitVersion);
+  }
+
+  public void git_version_does_not_support_sparse_checkout_default_rules() throws IOException, VcsException {
+    GitVersion gitVersion = GIT_WITH_SPARSE_CHECKOUT.previousVersion();
+    myVcsSupport =  vcsSupportWithFakeGitOfVersion(gitVersion);
+
+    VcsRoot vcsRoot = vcsRootWithAgentGitPath(gitVersion.toString());
+    AgentRunningBuild build = runningBuild().sharedConfigParams(PluginConfigImpl.USE_SPARSE_CHECKOUT, "true").addRoot(vcsRoot).build();
+
+    AgentCheckoutAbility canCheckout = myVcsSupport.canCheckout(vcsRoot, CheckoutRules.DEFAULT, build);
+    then(canCheckout.getCanNotCheckoutReason()).isNull();
   }
 
   public void should_check_auth_method() throws Exception {
@@ -123,14 +141,124 @@ public class AutoCheckoutTest extends BaseRemoteRepositoryTest {
     VcsRoot vcsRoot = vcsRoot()
       .withFetchUrl(getRemoteRepositoryUrl("repo.git"))
       .withAuthMethod(AuthenticationMethod.PRIVATE_KEY_FILE)
+      .withAgentGitPath(getGitPath())
       .build();
 
-    AgentCheckoutAbility canCheckout = myVcsSupport.canCheckout(vcsRoot, CheckoutRules.DEFAULT, runningBuild().build());
+    AgentCheckoutAbility canCheckout = myVcsSupport.canCheckout(vcsRoot, CheckoutRules.DEFAULT, runningBuild().addRoot(vcsRoot).build());
     then(canCheckout.getCanNotCheckoutReason().getType()).isEqualTo(AgentCanNotCheckoutReason.UNKNOWN_REASON_TYPE);
     then(canCheckout.getCanNotCheckoutReason().getDetails()).contains(
       "TeamCity doesn't support authentication method 'Private Key' with agent checkout. Please use different authentication method.");
   }
 
+  @DataProvider
+  public static Object[][] severalRootsSetups() throws Exception {
+    return new Object[][]{
+      new Object[]{new Setup()
+        .setShouldFail(true)
+      },
+      new Object[]{new Setup()
+        .setCheckoutRules1("+:dir")
+        .setCheckoutRules2("+:dir")
+        .setShouldFail(true)
+      },
+      new Object[]{new Setup()
+        .setCheckoutRules2("+:dir2")
+        .setShouldFail(true)
+      },
+      new Object[]{new Setup()
+        .setCheckoutRules2("+:dir1") //even though we checkout different dirs, .git of both repositories is located in the same dir
+        .setCheckoutRules2("+:dir2")
+        .setShouldFail(true)
+      },
+      new Object[]{new Setup()
+        .setCheckoutRules1("+:.=>dir1")
+        .setCheckoutRules2("+:.=>dir2")
+        .setShouldFail(false)
+      }
+    };
+  }
+
+
+  @TestFor(issues = "TW-49786")
+  @Test(dataProvider = "severalRootsSetups")
+  public void several_roots(@NotNull Setup setup) throws Exception {
+    myVcsSupport = vcsSupportWithRealGit();
+
+    VcsRoot root1 = vcsRoot().withId(1).withFetchUrl("http://some.org/repo1.git").withAgentGitPath(getGitPath()).build();
+    VcsRoot root2 = vcsRoot().withId(2).withFetchUrl("http://some.org/repo2.git").withAgentGitPath(getGitPath()).build();
+    AgentRunningBuild build = runningBuild()
+      .addRootEntry(root1, setup.getCheckoutRules1())
+      .addRootEntry(root2, setup.getCheckoutRules2())
+      .build();
+    AgentCheckoutAbility canCheckout1 = myVcsSupport.canCheckout(root1, new CheckoutRules(setup.getCheckoutRules1()), build);
+    AgentCheckoutAbility canCheckout2 = myVcsSupport.canCheckout(root2, new CheckoutRules(setup.getCheckoutRules2()), build);
+    if (setup.isShouldFail()) {
+      then(canCheckout1.getCanNotCheckoutReason().getDetails()).contains(
+        "Cannot checkout VCS root '" + root1.getName() + "' into the same directory as VCS root '" + root2.getName() + "'");
+      then(canCheckout2.getCanNotCheckoutReason().getDetails()).contains(
+        "Cannot checkout VCS root '" + root2.getName() + "' into the same directory as VCS root '" + root1.getName() + "'");
+    } else {
+      then(canCheckout1.getCanNotCheckoutReason()).isNull();
+      then(canCheckout2.getCanNotCheckoutReason()).isNull();
+    }
+  }
+
+
+  @TestFor(issues = "TW-49786")
+  public void should_respect_root_settings_when_checking_multi_root_constraints() throws Exception {
+    myVcsSupport = vcsSupportWithRealGit();
+
+    //second root has broken git path, we should not take it into account
+    //during canCheckout() for the first VCS root
+    VcsRoot root1 = vcsRoot().withId(1).withAgentGitPath(getGitPath()).withFetchUrl("http://some.org/repo1.git").build();
+    VcsRoot root2 = vcsRoot().withId(2).withAgentGitPath("wrongGitPath").withFetchUrl("http://some.org/repo2.git").build();
+    AgentRunningBuild build = runningBuild()
+      .addRootEntry(root1, "+:dir1")
+      .addRootEntry(root2, "+:dir2")
+      .build();
+
+    AgentCheckoutAbility canCheckout1 = myVcsSupport.canCheckout(root1, new CheckoutRules("+:dir1"), build);
+    AgentCheckoutAbility canCheckout2 = myVcsSupport.canCheckout(root2, new CheckoutRules("+:dir2"), build);
+    then(canCheckout1.getCanNotCheckoutReason()).isNull();
+    then(canCheckout2.getCanNotCheckoutReason().getType()).isEqualTo(AgentCanNotCheckoutReason.NO_VCS_CLIENT);
+    then(canCheckout2.getCanNotCheckoutReason().getDetails()).contains("Unable to run git at path wrongGitPath");
+  }
+
+
+  @TestFor(issues = "TW-49786")
+  public void should_respect_root_settings_when_checking_multi_root_constraints2() throws Exception {
+    VcsRoot root1 = vcsRoot().withId(1).withFetchUrl("http://some.org/repo1.git").build();
+    VcsRoot root2 = vcsRoot().withId(2).withFetchUrl("http://some.org/repo2.git").build();
+    AgentRunningBuild build = runningBuild()
+      .addRootEntry(root1, "+:dir1")
+      .addRootEntry(root2, "+:dir2")
+      .build();
+
+    //both roots require sparse checkout and mapped into the same directory, but the second
+    //root uses git version which doesn't support sparse checkout; we shouldn't take it into
+    //account during canCheckout() check for the first root
+    GitDetector detector = new GitDetector() {
+      @NotNull
+      public GitExec getGitPathAndVersion(@NotNull VcsRoot root, @NotNull BuildAgentConfiguration config, @NotNull AgentRunningBuild build) throws VcsException {
+        if (root.equals(root1)) {
+          return new GitExec("git1", GIT_WITH_SPARSE_CHECKOUT);
+        }
+        if (root.equals(root2)) {
+          return new GitExec("git2", GIT_WITH_SPARSE_CHECKOUT.previousVersion());
+        }
+        throw new VcsException("Unexpected VCS root");
+      }
+    };
+    myVcsSupport = createVcsSupport(detector);
+
+    AgentCheckoutAbility canCheckout1 = myVcsSupport.canCheckout(root1, new CheckoutRules("+:dir1"), build);
+    AgentCheckoutAbility canCheckout2 = myVcsSupport.canCheckout(root2, new CheckoutRules("+:dir2"), build);
+    then(canCheckout1.getCanNotCheckoutReason()).isNull();
+    then(canCheckout2.getCanNotCheckoutReason().getType()).isEqualTo(AgentCanNotCheckoutReason.NOT_SUPPORTED_CHECKOUT_RULES);
+    then(canCheckout2.getCanNotCheckoutReason().getDetails()).contains("Cannot perform sparse checkout using git " + GIT_WITH_SPARSE_CHECKOUT.previousVersion());
+  }
+
+
   private void verifyCanCheckout(final VcsRoot vcsRoot, CheckoutRules checkoutRules, final AgentRunningBuild build) throws VcsException {
     AgentCheckoutAbility canCheckout = myVcsSupport.canCheckout(vcsRoot, checkoutRules, build);
     then(canCheckout.getCanNotCheckoutReason()).isNull();
@@ -142,6 +270,10 @@ public class AutoCheckoutTest extends BaseRemoteRepositoryTest {
     then(myCheckoutDir.listFiles()).isNotEmpty();
   }
 
+  private VcsRoot vcsRootWithAgentGitPath() {
+    return vcsRootWithAgentGitPath(getGitPath());
+  }
+
   private VcsRoot vcsRootWithAgentGitPath(String path) {
     return vcsRoot().withBranch("refs/heads/master").withAgentGitPath(path).withFetchUrl(getRemoteRepositoryUrl("repo.git")).build();
   }
@@ -167,4 +299,48 @@ public class AutoCheckoutTest extends BaseRemoteRepositoryTest {
   private GitAgentVcsSupport createVcsSupport(final GitDetector detector) throws IOException {
     return new AgentSupportBuilder(myTempFiles).setGitDetector(detector).build();
   }
+
+
+  private static class Setup {
+    private String myCheckoutRules1 = CheckoutRules.DEFAULT.getAsString();
+    private String myCheckoutRules2 = CheckoutRules.DEFAULT.getAsString();
+    private boolean myShouldFail;
+
+    @NotNull
+    public String getCheckoutRules1() {
+      return myCheckoutRules1;
+    }
+
+    @NotNull
+    public Setup setCheckoutRules1(@NotNull String checkoutRules1) {
+      myCheckoutRules1 = checkoutRules1;
+      return this;
+    }
+
+    @NotNull
+    public String getCheckoutRules2() {
+      return myCheckoutRules2;
+    }
+
+    @NotNull
+    public Setup setCheckoutRules2(@NotNull String checkoutRules2) {
+      myCheckoutRules2 = checkoutRules2;
+      return this;
+    }
+
+    public boolean isShouldFail() {
+      return myShouldFail;
+    }
+
+    @NotNull
+    public Setup setShouldFail(boolean shouldFail) {
+      myShouldFail = shouldFail;
+      return this;
+    }
+
+    @Override
+    public String toString() {
+      return "rules1: '" + myCheckoutRules1 + "', rules2: '" + myCheckoutRules2 + "'";
+    }
+  }
 }
index 47ab5128018fdd3bd300d8c5e1c9d52c5d43a7fd..2e742517d89e05f8c809a66a568053ec704cf6e7 100644 (file)
@@ -20,9 +20,12 @@ import jetbrains.buildServer.BaseTestCase;
 import jetbrains.buildServer.TempFiles;
 import jetbrains.buildServer.buildTriggers.vcs.git.*;
 import jetbrains.buildServer.serverSide.ServerPaths;
+import jetbrains.buildServer.util.FileUtil;
 import jetbrains.buildServer.vcs.CheckoutRules;
 import jetbrains.buildServer.vcs.VcsException;
 import jetbrains.buildServer.vcs.VcsRoot;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.RepositoryBuilder;
 import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.DataProvider;
@@ -33,6 +36,7 @@ import java.io.FileFilter;
 import java.io.IOException;
 
 import static jetbrains.buildServer.buildTriggers.vcs.git.tests.GitSupportBuilder.gitSupport;
+import static org.assertj.core.api.BDDAssertions.then;
 
 @Test
 public class CleanerTest extends BaseTestCase {
@@ -47,7 +51,7 @@ public class CleanerTest extends BaseTestCase {
   @BeforeMethod
   public void setUp() throws IOException {
     ServerPaths paths = new ServerPaths(ourTempFiles.createTempDir().getAbsolutePath());
-    myConfigBuilder = new PluginConfigBuilder(paths).setMirrorExpirationTimeoutMillis(5000);
+    myConfigBuilder = new PluginConfigBuilder(paths);
 
     if (System.getenv(Constants.TEAMCITY_AGENT_GIT_PATH) != null)
       myConfigBuilder.setPathToGit(System.getenv(Constants.TEAMCITY_AGENT_GIT_PATH));
@@ -61,6 +65,7 @@ public class CleanerTest extends BaseTestCase {
 
   @Test(dataProvider = "true,false")
   public void test_clean(Boolean useJgitGC) throws VcsException, InterruptedException {
+    myConfigBuilder.setMirrorExpirationTimeoutMillis(5000);
     if (useJgitGC) {
       myConfigBuilder.setRunJGitGC(true);
       myConfigBuilder.setRunNativeGC(false);
@@ -95,12 +100,40 @@ public class CleanerTest extends BaseTestCase {
   }
 
 
+  public void nonInplaceGc() throws Exception {
+    myConfigBuilder.setRunNativeGC(true);
+    myConfigBuilder.setRunInPlaceGc(false);
+    initCleanup();
+
+    VcsRoot root = GitTestUtil.getVcsRoot();
+    //clone repository
+    mySupport.collectChanges(root, "70dbcf426232f7a33c7e5ebdfbfb26fc8c467a46", "a894d7d58ffde625019a9ecf8267f5f1d1e5c341", CheckoutRules.DEFAULT);
+    File repositoryDir = getRepositoryDir(root);
+
+    //create more than 50 packs to trigger gc:
+    File packDir = new File(repositoryDir, "objects/pack");
+    File pack = new File(packDir, "pack-3763fffad1c368b0a79f9a196ee098e303fc0c29.pack");
+    File idx = new File(packDir, "pack-3763fffad1c368b0a79f9a196ee098e303fc0c29.idx");
+    for (int i = 10; i <= 60; i++) {
+      FileUtil.copy(pack, new File(packDir, "pack-" + i + "63fffad1c368b0a79f9a196ee098e303fc0c29.pack"));
+      FileUtil.copy(idx, new File(packDir, "pack-" + i + "63fffad1c368b0a79f9a196ee098e303fc0c29.idx"));
+    }
+    FileRepository db = (FileRepository) new RepositoryBuilder().setGitDir(repositoryDir).build();
+    then(db.getObjectDatabase().getPacks().size() > 50).isTrue();
+
+    myCleanup.run();
+
+    db = (FileRepository) new RepositoryBuilder().setGitDir(repositoryDir).build();
+    then(db.getObjectDatabase().getPacks().size()).isEqualTo(1);
+  }
+
+
   private void initCleanup() {
     myConfig = myConfigBuilder.build();
     GitSupportBuilder gitBuilder = gitSupport().withPluginConfig(myConfig);
     mySupport = gitBuilder.build();
     myRepositoryManager = gitBuilder.getRepositoryManager();
-    myCleanup = new Cleanup(myConfig, myRepositoryManager);
+    myCleanup = new Cleanup(myConfig, myRepositoryManager, new GcErrors());
   }
 
 
index afc1c353ab738b228643472642b5c14de54a60ac..6032aaddd57046689bf76dc6f5c4a9dece8437ab 100644 (file)
@@ -248,7 +248,7 @@ public class CollectChangesTest extends BaseRemoteRepositoryTest {
     ServerPluginConfig config = myConfig.build();
     MirrorManager mirrorManager = new MirrorManagerImpl(config, new HashCalculatorImpl());
     RepositoryManager repositoryManager = new RepositoryManagerImpl(config, mirrorManager);
-    ResetCacheHandler resetHandler = new GitResetCacheHandler(repositoryManager);
+    ResetCacheHandler resetHandler = new GitResetCacheHandler(repositoryManager, new GcErrors());
     for (String cache : resetHandler.listCaches())
       resetHandler.resetCache(cache);
 
index 6ff6f3d59d299d34228bb6c3a89dd170d536b524..8424fa0be9ab7e1341d14d602ead88c9440fc679 100644 (file)
@@ -223,6 +223,52 @@ public class GitPatchTest extends PatchTestCase {
   }
 
 
+  @TestFor(issues = "TW-49782")
+  @Test(dataProvider = "patchInSeparateProcess")
+  public void excluded_broken_submodule(boolean patchInSeparateProcess) throws Exception {
+    myConfigBuilder.setSeparateProcessForPatch(patchInSeparateProcess);
+    VcsRoot root = getRoot("reference-wrong-commit", true);
+    //7253d358a2490321a1808a1c20561b4027d69f77 references wrong submodule commit, but it is excluded by checkout rules, patch should succeed
+    checkPatch(root, "excluded_broken_submodule", null, "7253d358a2490321a1808a1c20561b4027d69f77", new CheckoutRules("+:dir"));
+  }
+
+
+  @TestFor(issues = "TW-50097")
+  @Test(dataProvider = "patchInSeparateProcess")
+  public void submodules_and_checkout_rules(boolean patchInSeparateProcess) throws Exception {
+    myConfigBuilder.setSeparateProcessForPatch(patchInSeparateProcess);
+    VcsRoot root = getRoot("sub-submodule", true);
+    checkPatch(root, "submodules_and_checkout_rules", null, "ce6044093939bb47283439d97a1c80f759669ff5", new CheckoutRules("+:first-level-submodule"));
+  }
+
+
+  @TestFor(issues = "TW-50097")
+  @Test(dataProvider = "patchInSeparateProcess")
+  public void submodules_and_checkout_rules2(boolean patchInSeparateProcess) throws Exception {
+    myConfigBuilder.setSeparateProcessForPatch(patchInSeparateProcess);
+    VcsRoot root = getRoot("sub-submodule", true);
+    checkPatch(root, "submodules_and_checkout_rules2", null, "ce6044093939bb47283439d97a1c80f759669ff5", new CheckoutRules("+:first-level-submodule/sub-sub"));
+  }
+
+
+  @TestFor(issues = "TW-50097")
+  @Test(dataProvider = "patchInSeparateProcess")
+  public void submodules_and_checkout_rules3(boolean patchInSeparateProcess) throws Exception {
+    myConfigBuilder.setSeparateProcessForPatch(patchInSeparateProcess);
+    VcsRoot root = getRoot("sub-submodule", true);
+    checkPatch(root, "submodules_and_checkout_rules3", null, "ce6044093939bb47283439d97a1c80f759669ff5", new CheckoutRules("-:first-level-submodule/sub-sub/file.txt"));
+  }
+
+
+  @TestFor(issues = "TW-50097")
+  @Test(dataProvider = "patchInSeparateProcess")
+  public void submodules_and_checkout_rules4(boolean patchInSeparateProcess) throws Exception {
+    myConfigBuilder.setSeparateProcessForPatch(patchInSeparateProcess);
+    VcsRoot root = getRoot("sub-submodule", true);
+    checkPatch(root, "submodules_and_checkout_rules4", null, "ce6044093939bb47283439d97a1c80f759669ff5", new CheckoutRules("+:first-level-submodule/sub-sub/file.txt"));
+  }
+
+
   @Test(dataProvider = "patchInSeparateProcess")
   public void should_build_patch_on_revision_in_branch_when_cache_is_empty(boolean patchInSeparateProcess) throws Exception {
     myConfigBuilder.setSeparateProcessForPatch(patchInSeparateProcess);
index cc4a385ba243d31b29b9aaffe0cc21d6a8c799b9..3e5ede080ecc8d6e0fe6c68a2614ec4de47e4782 100644 (file)
@@ -17,6 +17,7 @@
 package jetbrains.buildServer.buildTriggers.vcs.git.tests;
 
 import jetbrains.buildServer.TempFiles;
+import jetbrains.buildServer.buildTriggers.vcs.git.GcErrors;
 import jetbrains.buildServer.buildTriggers.vcs.git.GitResetCacheHandler;
 import jetbrains.buildServer.buildTriggers.vcs.git.RepositoryManager;
 import jetbrains.buildServer.serverSide.BasePropertiesModel;
@@ -61,7 +62,7 @@ public class GitResetCacheHandlerTest {
     myTempFiles = new TempFiles();
     myCachesDir = myTempFiles.createTempDir();
     myRepositoryManager = myContext.mock(RepositoryManager.class);
-    myCacheHandler = new GitResetCacheHandler(myRepositoryManager);
+    myCacheHandler = new GitResetCacheHandler(myRepositoryManager, new GcErrors());
   }
 
   @AfterMethod
index fd8300804e409e75a13d59edebe9c56190ca77c1..6d6a5f2d5e53cfe95f7d9cfca33d9e76636fb88b 100644 (file)
@@ -19,9 +19,12 @@ package jetbrains.buildServer.buildTriggers.vcs.git.tests;
 import jetbrains.buildServer.TempFiles;
 import jetbrains.buildServer.buildTriggers.vcs.git.GitServerUtil;
 import jetbrains.buildServer.util.FileUtil;
+import jetbrains.buildServer.util.TestFor;
+import jetbrains.buildServer.vcs.VcsException;
 import junit.framework.TestCase;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.URIish;
+import org.jetbrains.annotations.NotNull;
 import org.testng.annotations.AfterMethod;
 import org.testng.annotations.Test;
 
@@ -69,4 +72,17 @@ public class GitServerUtilTest extends TestCase {
     assertEquals(GitServerUtil.GB, (long)GitServerUtil.convertMemorySizeToBytes("1G"));
     assertEquals(2 * GitServerUtil.GB, (long)GitServerUtil.convertMemorySizeToBytes("2G"));
   }
+
+
+  @TestFor(issues = "TW-50043")
+  @Test(dataProviderClass = GitVcsRootTest.class, dataProvider = "urlsWithNewLines")
+  public void url_with_newline(@NotNull String url) throws Exception {
+    File dir = myTempFiles.createTempDir();
+    try {
+      GitServerUtil.getRepository(dir, new URIish(url));
+      fail("No error for url '" + url + "'");
+    } catch (VcsException e) {
+      //expected
+    }
+  }
 }
index 96a23e168da3d07338bbd99c05f8923ef8132676..a55e166df70b75a44dbfef07f6e66888d1640e18 100644 (file)
@@ -99,7 +99,7 @@ public class GitSupportBuilder {
     RevisionsCache revisionsCache = new RevisionsCache(myPluginConfig);
     myMapFullPath = new GitMapFullPath(myPluginConfig, revisionsCache);
     myCommitLoader = new CommitLoaderImpl(myRepositoryManager, myFetchCommand, myMapFullPath);
-    GitResetCacheHandler resetCacheHandler = new GitResetCacheHandler(myRepositoryManager);
+    GitResetCacheHandler resetCacheHandler = new GitResetCacheHandler(myRepositoryManager, new GcErrors());
     ResetRevisionsCacheHandler resetRevisionsCacheHandler = new ResetRevisionsCacheHandler(revisionsCache);
     GitVcsSupport git = new GitVcsSupport(myPluginConfig, resetCacheManager, myTransportFactory, myRepositoryManager, myMapFullPath, myCommitLoader,
                                           new EmptyVcsRootSshKeyManager(), new MockVcsOperationProgressProvider(),
index 116ea979ed804dcff8f81777cb1bab780d4bdaef..3c57aeaf580bd931008306f5c0b5c651066f4064 100644 (file)
@@ -199,6 +199,12 @@ public class GitUrlSupportTest extends BaseTestCase {
     }
   }
 
+  @Test
+  public void should_adjust_gitlab_fetch_url() throws VcsException {
+    VcsUrl url = new VcsUrl("https://gitlab.com/fdroid/repomaker");
+    GitVcsRoot root = toGitRoot(url);
+    assertEquals("https://gitlab.com/fdroid/repomaker.git", root.getProperty(Constants.FETCH_URL));
+  }
 
   private void checkAuthMethod(MavenVcsUrl url, GitVcsRoot root) {
     if (url.getProviderSpecificPart().startsWith("ssh")) {
index 8b0033cbe2920ba1bfec181d68e964cc38635d8f..8fee6ecf2a019daa117a4aaeca58213fa7f20a82 100644 (file)
@@ -23,18 +23,19 @@ import jetbrains.buildServer.serverSide.ServerPaths;
 import jetbrains.buildServer.serverSide.TeamCityProperties;
 import jetbrains.buildServer.util.FileUtil;
 import jetbrains.buildServer.util.TestFor;
+import jetbrains.buildServer.vcs.VcsException;
 import jetbrains.buildServer.vcs.VcsRoot;
 import org.eclipse.jgit.transport.URIish;
+import org.jetbrains.annotations.NotNull;
 import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
 import java.io.File;
 import java.io.IOException;
 
 import static jetbrains.buildServer.buildTriggers.vcs.git.tests.VcsRootBuilder.vcsRoot;
-import static org.testng.AssertJUnit.assertEquals;
-import static org.testng.AssertJUnit.assertNull;
-import static org.testng.AssertJUnit.assertTrue;
+import static org.testng.AssertJUnit.*;
 
 /**
  * @author dmitry.neverov
@@ -86,4 +87,37 @@ public class GitVcsRootTest {
       System.getProperties().remove(Constants.CUSTOM_CLONE_PATH_ENABLED);
     }
   }
+
+  @DataProvider(name = "urlsWithNewLines")
+  public static Object[][] urlsWithNewLines() {
+    return new Object[][] {
+      new Object[] { "http://some.org/repo\n" },
+      new Object[] { "http://some.org/repo\r" },
+      new Object[] { "http://some.org/repo\n[section]" },
+      new Object[] { "http://some.org/repo\r[section]" },
+      new Object[] { "http://some.org/repo\r\n[section]" },
+    };
+  }
+
+  @TestFor(issues = "TW-50043")
+  @Test(dataProvider = "urlsWithNewLines")
+  public void new_line_in_fetch_url(@NotNull String url) throws VcsException {
+    try {
+      new GitVcsRoot(myMirrorManager, vcsRoot().withFetchUrl(url).build());
+      fail("No error for url '" + url + "'");
+    } catch (VcsException e) {
+      //expected
+    }
+  }
+
+  @TestFor(issues = "TW-50043")
+  @Test(dataProvider = "urlsWithNewLines")
+  public void new_line_in_push_url(@NotNull String url) throws VcsException {
+    try {
+      new GitVcsRoot(myMirrorManager, vcsRoot().withFetchUrl("http://some.org/repo.git").withPushUrl(url).build());
+      fail("No error for url '" + url + "'");
+    } catch (VcsException e) {
+      //expected
+    }
+  }
 }
diff --git a/git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/HttpUrlWithUsernameTest.java b/git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/HttpUrlWithUsernameTest.java
new file mode 100644 (file)
index 0000000..b3dc47e
--- /dev/null
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2000-2017 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.agent.AgentRunningBuild;
+import jetbrains.buildServer.buildTriggers.vcs.git.AuthenticationMethod;
+import jetbrains.buildServer.buildTriggers.vcs.git.Constants;
+import jetbrains.buildServer.buildTriggers.vcs.git.GitVcsRoot;
+import jetbrains.buildServer.buildTriggers.vcs.git.MirrorManagerImpl;
+import jetbrains.buildServer.buildTriggers.vcs.git.agent.GitAgentVcsSupport;
+import jetbrains.buildServer.buildTriggers.vcs.git.agent.PluginConfigImpl;
+import jetbrains.buildServer.util.FileUtil;
+import jetbrains.buildServer.util.TestFor;
+import jetbrains.buildServer.vcs.CheckoutRules;
+import jetbrains.buildServer.vcs.VcsException;
+import jetbrains.buildServer.vcs.impl.VcsRootImpl;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryBuilder;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.jetbrains.annotations.NotNull;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import static jetbrains.buildServer.buildTriggers.vcs.git.tests.GitTestUtil.copyRepository;
+import static jetbrains.buildServer.buildTriggers.vcs.git.tests.GitTestUtil.dataFile;
+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;
+
+@SuppressWarnings("ALL")
+@Test
+@TestFor(issues = "TW-48103")
+public class HttpUrlWithUsernameTest extends BaseRemoteRepositoryTest {
+
+  private final static String USER = "user";
+  private final static String PASSWORD = "pwd";
+
+  private AgentSupportBuilder myBuilder;
+  private GitAgentVcsSupport myVcsSupport;
+  private MirrorManagerImpl myMirrorManager;
+  private GitHttpServer myServer;
+  private String myGitPath;
+  private File myBuildDir;
+
+  @Override
+  @BeforeMethod
+  public void setUp() throws Exception {
+    super.setUp();
+
+    myBuilder = new AgentSupportBuilder(myTempFiles);
+    myVcsSupport = myBuilder.build();
+    myMirrorManager = myBuilder.getMirrorManager();
+    myGitPath = GitVersionProvider.getGitPath();
+    myBuildDir = myTempFiles.createTempDir();
+  }
+
+
+  @Override
+  @AfterMethod
+  public void tearDown() {
+    super.tearDown();
+    if (myServer != null)
+      myServer.stop();
+  }
+
+
+  @Test(dataProvider = "mirrorModes")
+  public void should_include_username_into_url_when_asked(@NotNull MirrorMode mirrorMode) throws Exception {
+    //need that in order to be able to disable new logic in case of any problems
+
+    File repo = copyRepository(myTempFiles, dataFile("repo_for_fetch.1"), "repo.git");
+    startGitServer(repo);
+    VcsRootImpl root = createRoot();
+    AgentRunningBuild build = createBuild(configParams(mirrorMode, PluginConfigImpl.EXCLUDE_USERNAME_FROM_HTTP_URL, "false"));
+    checkout(root, build, "add81050184d3c818560bdd8839f50024c188586");
+
+    if (mirrorMode != MirrorMode.DISABLED) {
+      StoredConfig config = getMirrorConfig(root);
+      then(config.getString("remote", "origin", "url")).contains(USER + "@");
+    }
+
+    StoredConfig config = getWorkingDirConfig();
+    then(config.getString("remote", "origin", "url")).contains(USER + "@");
+  }
+
+
+  @Test(dataProvider = "mirrorModes")
+  public void no_username_in_http_urls(@NotNull MirrorMode mirrorMode) throws Exception {
+    File repo = copyRepository(myTempFiles, dataFile("repo_for_fetch.1"), "repo.git");
+    startGitServer(repo);
+    VcsRootImpl root = createRoot();
+    AgentRunningBuild build = createBuild(configParams(mirrorMode));
+    checkout(root, build, "add81050184d3c818560bdd8839f50024c188586");
+
+    if (mirrorMode != MirrorMode.DISABLED) {
+      StoredConfig config = getMirrorConfig(root);
+      then(config.getString("remote", "origin", "url")).doesNotContain(USER + "@");
+      then(config.getString("credential", null, "username")).isEqualTo(USER);
+    }
+
+    StoredConfig config = getWorkingDirConfig();
+    then(config.getString("remote", "origin", "url")).doesNotContain(USER + "@");
+    then(config.getString("credential", null, "username")).isEqualTo(USER);
+  }
+
+
+  @Test(dataProvider = "mirrorModes")
+  public void no_username_in_http_urls_upgrade(@NotNull MirrorMode mirrorMode) throws Exception {
+    //run first build to initialize fetch urls with usernames
+    File repo = copyRepository(myTempFiles, dataFile("repo_for_fetch.1"), "repo.git");
+    startGitServer(repo);
+    VcsRootImpl root = createRoot();
+    AgentRunningBuild build1 = createBuild(configParams(mirrorMode, PluginConfigImpl.EXCLUDE_USERNAME_FROM_HTTP_URL, "false"));
+    checkout(root, build1, "add81050184d3c818560bdd8839f50024c188586");
+
+    //update remote repo to cause fetch
+    FileUtil.delete(repo);
+    copyRepository(dataFile("repo_for_fetch.2"), repo);
+
+    AgentRunningBuild build = createBuild(configParams(mirrorMode));
+    checkout(root, build, "d47dda159b27b9a8c4cee4ce98e4435eb5b17168");
+
+    if (mirrorMode != MirrorMode.DISABLED) {
+      StoredConfig config = getMirrorConfig(root);
+      then(config.getString("remote", "origin", "url")).doesNotContain(USER + "@");
+      then(config.getString("credential", null, "username")).isEqualTo(USER);
+    }
+
+    StoredConfig config = getWorkingDirConfig();
+    then(config.getString("remote", "origin", "url")).doesNotContain(USER + "@");
+    then(config.getString("credential", null, "username")).isEqualTo(USER);
+  }
+
+
+  private void checkout(@NotNull VcsRootImpl root, @NotNull AgentRunningBuild build, @NotNull String revision) throws VcsException {
+    myVcsSupport.updateSources(root, CheckoutRules.DEFAULT, revision, myBuildDir, build, false);
+  }
+
+
+  @NotNull
+  private StoredConfig getWorkingDirConfig() throws IOException {
+    Repository r = new RepositoryBuilder().setWorkTree(myBuildDir).build();
+    return r.getConfig();
+  }
+
+
+  @NotNull
+  private StoredConfig getMirrorConfig(@NotNull VcsRootImpl root) throws IOException, VcsException {
+    GitVcsRoot gitRoot = new GitVcsRoot(myMirrorManager, root);
+    File mirrorDir = myMirrorManager.getMirrorDir(gitRoot.getRepositoryFetchURL().toString());
+    Repository r = new RepositoryBuilder().setGitDir(mirrorDir).build();
+    return r.getConfig();
+  }
+
+
+  @NotNull
+  private AgentRunningBuild createBuild(@NotNull Map<String, String> configParams) {
+    return runningBuild()
+      .sharedEnvVariable(Constants.TEAMCITY_AGENT_GIT_PATH, myGitPath)
+      .sharedConfigParams(configParams)
+      .build();
+  }
+
+
+  @NotNull
+  private VcsRootImpl createRoot() {
+    return vcsRoot()
+      .withFetchUrl(myServer.getRepoUrl())
+      .withAuthMethod(AuthenticationMethod.PASSWORD)
+      .withUsername(USER)
+      .withPassword(PASSWORD)
+      .withBranch("master")
+      .build();
+  }
+
+
+  private void startGitServer(@NotNull File repo) throws IOException {
+    myServer = new GitHttpServer(myGitPath, repo);
+    myServer.setCredentials(USER, PASSWORD);
+    myServer.start();
+  }
+
+
+  @NotNull
+  private Map<String, String> configParams(@NotNull MirrorMode mirrorMode, String... params) {
+    Map<String, String> result = new HashMap<>();
+    switch (mirrorMode) {
+      case MIRROR:
+        result.put(PluginConfigImpl.USE_MIRRORS, "true");
+        break;
+      case ALTERNATES:
+        result.put(PluginConfigImpl.USE_ALTERNATES, "true");
+    }
+    if (params.length != 0)
+      result.putAll(map(params));
+    return result;
+  }
+
+
+  @DataProvider
+  public static Object[][] mirrorModes() throws Exception {
+    Object[][] result = new Object[MirrorMode.values().length][];
+    int i = 0;
+    for (MirrorMode mode : MirrorMode.values()) {
+      result[i] = new Object[]{mode};
+      i++;
+    }
+    return result;
+  }
+
+  private enum MirrorMode {
+    DISABLED,
+    MIRROR,
+    ALTERNATES
+  }
+}
index 64eb4783021a8df2f7a8cfcfd122dac8fecbcdff..175effefa78da8491c6e80e74f2fe6cae2ea2e09 100644 (file)
@@ -21,6 +21,7 @@ import jetbrains.buildServer.buildTriggers.vcs.git.CommitLoader;
 import jetbrains.buildServer.buildTriggers.vcs.git.GitMapFullPath;
 import jetbrains.buildServer.buildTriggers.vcs.git.GitVcsSupport;
 import jetbrains.buildServer.buildTriggers.vcs.git.OperationContext;
+import jetbrains.buildServer.log.LogInitializer;
 import jetbrains.buildServer.serverSide.BasePropertiesModel;
 import jetbrains.buildServer.serverSide.ServerPaths;
 import jetbrains.buildServer.serverSide.TeamCityProperties;
@@ -39,16 +40,15 @@ import org.jetbrains.annotations.NotNull;
 import org.jmock.Expectations;
 import org.jmock.Mockery;
 import org.jmock.lib.legacy.ClassImposteriser;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.DataProvider;
-import org.testng.annotations.Test;
+import org.testng.annotations.*;
 
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
 import java.util.Collection;
+import java.util.List;
 
+import static java.util.Arrays.asList;
 import static jetbrains.buildServer.buildTriggers.vcs.git.tests.GitSupportBuilder.gitSupport;
 import static jetbrains.buildServer.buildTriggers.vcs.git.tests.GitTestUtil.copyRepository;
 import static jetbrains.buildServer.buildTriggers.vcs.git.tests.GitTestUtil.dataFile;
@@ -70,6 +70,11 @@ public class MapFullPathTest {
   private VcsRoot myRoot2;
   private VcsRootEntry myRootEntry2;
 
+  @BeforeClass
+  public void setUpClass() {
+    LogInitializer.setUnitTest(true);
+  }
+
   @BeforeMethod
   public void setUp() throws IOException {
     new TeamCityProperties() {{
@@ -88,9 +93,9 @@ public class MapFullPathTest {
     GitSupportBuilder gitBuilder = gitSupport().withServerPaths(paths);
     myGit = gitBuilder.build();
     myMapFullPath = gitBuilder.getMapFullPath();
-    myRoot = vcsRoot().withFetchUrl(myRemoteRepositoryDir.getAbsolutePath()).build();
+    myRoot = vcsRoot().withId(1).withFetchUrl(myRemoteRepositoryDir.getAbsolutePath()).build();
     myRootEntry = new VcsRootEntry(myRoot, CheckoutRules.DEFAULT);
-    myRoot2 = vcsRoot().withFetchUrl(myRemoteRepositoryDir2.getAbsolutePath()).build();
+    myRoot2 = vcsRoot().withId(2).withFetchUrl(myRemoteRepositoryDir2.getAbsolutePath()).build();
     myRootEntry2 = new VcsRootEntry(myRoot2, CheckoutRules.DEFAULT);
   }
 
@@ -191,6 +196,32 @@ public class MapFullPathTest {
   }
 
 
+  public void bulk() throws Exception {
+    //clone repository for myRoot and root3
+    RepositoryStateData state0 = RepositoryStateData.createSingleVersionState("a7274ca8e024d98c7d59874f19f21d26ee31d41d");
+    RepositoryStateData state1 = myGit.getCurrentState(myRoot);
+    myGit.getCollectChangesPolicy().collectChanges(myRoot, state0, state1, CheckoutRules.DEFAULT);
+
+    VcsRoot root3 = vcsRoot().withId(3).withFetchUrl(myRemoteRepositoryDir.getAbsolutePath()).build();//tracks same repo as myRoot1
+    VcsRoot root4 = vcsRoot().withId(4).withFetchUrl(myRemoteRepositoryDir2.getAbsolutePath()).build();//tracks same repo as myRoot2
+
+    List<Boolean> result = myGit.checkSuitable(asList(
+      new VcsRootEntry(myRoot, new CheckoutRules("+:dir1")),
+      new VcsRootEntry(myRoot, new CheckoutRules("+:dir2")),
+      new VcsRootEntry(myRoot2, new CheckoutRules("+:dir2")),
+      new VcsRootEntry(root3, new CheckoutRules("+:dir3")),
+      new VcsRootEntry(root4, new CheckoutRules("+:dir4")),
+      new VcsRootEntry(root3, new CheckoutRules("+:dir5")),
+      new VcsRootEntry(root4, new CheckoutRules("+:dir6"))
+      ), asList(
+      "a7274ca8e024d98c7d59874f19f21d26ee31d41d-add81050184d3c818560bdd8839f50024c188586||.",//affects root and root3
+      "abababababababababababababababababababab||.")//affects no repo
+    );
+
+    then(result).containsExactly(true, true, false, true, false, true, false);
+  }
+
+
   @DataProvider
   public Object[][] fetchAction() {
     return new Object[][]{
index 3f094439aca97a1c143fefb60be27cedb22ce68d..c9f3ecba538c359c579b681ca5f8b25e692b89ee 100644 (file)
@@ -70,6 +70,7 @@ public class PluginConfigBuilder {
   private Boolean myNewConnectionForPrune;
   private Boolean myIgnoreMissingRemoteRef;
   private Integer myMergeRetryAttempts;
+  private Boolean myRunInPlaceGc;