2 * Copyright 2000-2014 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package jetbrains.buildServer.buildTriggers.vcs.git.agent;
19 import com.intellij.openapi.util.Trinity;
20 import jetbrains.buildServer.agent.AgentRunningBuild;
21 import jetbrains.buildServer.agent.BuildDirectoryCleanerCallback;
22 import jetbrains.buildServer.agent.BuildProgressLogger;
23 import jetbrains.buildServer.agent.SmartDirectoryCleaner;
24 import jetbrains.buildServer.buildTriggers.vcs.git.*;
25 import jetbrains.buildServer.buildTriggers.vcs.git.agent.command.*;
26 import jetbrains.buildServer.buildTriggers.vcs.git.agent.command.impl.CommandUtil;
27 import jetbrains.buildServer.buildTriggers.vcs.git.agent.command.impl.RefImpl;
28 import jetbrains.buildServer.buildTriggers.vcs.git.agent.errors.GitExecTimeout;
29 import jetbrains.buildServer.buildTriggers.vcs.git.agent.errors.GitIndexCorruptedException;
30 import jetbrains.buildServer.buildTriggers.vcs.git.agent.errors.GitOutdatedIndexException;
31 import jetbrains.buildServer.log.Loggers;
32 import jetbrains.buildServer.util.FileUtil;
33 import jetbrains.buildServer.vcs.*;
34 import org.apache.log4j.Logger;
35 import org.eclipse.jgit.errors.ConfigInvalidException;
36 import org.eclipse.jgit.lib.*;
37 import org.eclipse.jgit.lib.Constants;
38 import org.eclipse.jgit.transport.URIish;
39 import org.jetbrains.annotations.NotNull;
40 import org.jetbrains.annotations.Nullable;
43 import java.io.FileFilter;
44 import java.io.IOException;
45 import java.net.URISyntaxException;
46 import java.util.Collection;
47 import java.util.HashSet;
50 import java.util.regex.Matcher;
52 import static com.intellij.openapi.util.text.StringUtil.isEmpty;
53 import static jetbrains.buildServer.buildTriggers.vcs.git.GitUtils.*;
55 public class UpdaterImpl implements Updater {
57 private final static Logger LOG = Logger.getLogger(UpdaterImpl.class);
58 /** Git version which supports --progress option in the fetch command */
59 private final static GitVersion GIT_WITH_PROGRESS_VERSION = new GitVersion(1, 7, 1, 0);
60 //--force option in git submodule update introduced in 1.7.6
61 private final static GitVersion GIT_WITH_FORCE_SUBMODULE_UPDATE = new GitVersion(1, 7, 6);
62 public final static GitVersion GIT_WITH_SPARSE_CHECKOUT = new GitVersion(1, 7, 4);
63 public final static GitVersion BROKEN_SPARSE_CHECKOUT = new GitVersion(2, 7, 0);
65 * Git version supporting an empty credential helper - the only way to disable system/global/local cred helper
67 public final static GitVersion EMPTY_CRED_HELPER = new GitVersion(2, 9, 0);
68 /** Git version supporting [credential] section in config (the first version including a6fc9fd3f4b42cd97b5262026e18bd451c28ee3c) */
69 public final static GitVersion CREDENTIALS_SECTION_VERSION = new GitVersion(1, 7, 10);
71 private static final int SILENT_TIMEOUT = 24 * 60 * 60; //24 hours
73 protected final FS myFS;
74 private final SmartDirectoryCleaner myDirectoryCleaner;
75 protected final BuildProgressLogger myLogger;
76 protected final AgentPluginConfig myPluginConfig;
77 protected final GitFactory myGitFactory;
78 protected final File myTargetDirectory;
79 protected final String myRevision;
80 protected final AgentGitVcsRoot myRoot;
81 protected final String myFullBranchName;
82 protected final AgentRunningBuild myBuild;
83 private final CheckoutRules myRules;
84 private final CheckoutMode myCheckoutMode;
85 protected final MirrorManager myMirrorManager;
86 //remote repository refs, stored in field in order to not run 'git ls-remote' command twice
87 private Refs myRemoteRefs;
89 public UpdaterImpl(@NotNull FS fs,
90 @NotNull AgentPluginConfig pluginConfig,
91 @NotNull MirrorManager mirrorManager,
92 @NotNull SmartDirectoryCleaner directoryCleaner,
93 @NotNull GitFactory gitFactory,
94 @NotNull AgentRunningBuild build,
95 @NotNull VcsRoot root,
96 @NotNull String version,
97 @NotNull File targetDir,
98 @NotNull CheckoutRules rules,
99 @NotNull CheckoutMode checkoutMode) throws VcsException {
101 myPluginConfig = pluginConfig;
102 myDirectoryCleaner = directoryCleaner;
103 myGitFactory = gitFactory;
105 myLogger = build.getBuildLogger();
106 myRevision = GitUtils.versionRevision(version);
107 myTargetDirectory = targetDir;
108 myRoot = new AgentGitVcsRoot(mirrorManager, myTargetDirectory, root);
109 myFullBranchName = getBranch();
111 myCheckoutMode = checkoutMode;
112 myMirrorManager = mirrorManager;
116 private String getBranch() {
117 String defaultBranchName = GitUtils.expandRef(myRoot.getRef());
118 String rootBranchParam = GitUtils.getGitRootBranchParamName(myRoot.getOriginalRoot());
119 String customBranch = myBuild.getSharedConfigParameters().get(rootBranchParam);
120 return customBranch != null ? customBranch : defaultBranchName;
124 public void update() throws VcsException {
125 String msg = "Git version: " + myPluginConfig.getGitVersion();
126 myLogger.message(msg);
128 checkAuthMethodIsSupported();
132 protected void doUpdate() throws VcsException {
133 String message = "Update checkout directory (" + myTargetDirectory.getAbsolutePath() + ")";
134 myLogger.activityStarted(message, GitBuildProgressLogger.GIT_PROGRESS_ACTIVITY);
138 removeRefLocks(new File(myTargetDirectory, ".git"));
142 myLogger.activityFinished(message, GitBuildProgressLogger.GIT_PROGRESS_ACTIVITY);
146 private void logStartUpdating() {
147 LOG.info("Starting update of root " + myRoot.getName() + " in " + myTargetDirectory + " to revision " + myRevision);
148 LOG.debug("Updating " + myRoot.debugInfo());
152 private void initGitRepository() throws VcsException {
153 if (!new File(myTargetDirectory, ".git").exists()) {
157 configureRemoteUrl(new File(myTargetDirectory, ".git"));
158 setupExistingRepository();
159 configureSparseCheckout();
160 } catch (Exception e) {
161 LOG.warn("Do clean checkout due to errors while configure use of local mirrors", e);
165 removeOrphanedIdxFiles(new File(myTargetDirectory, ".git"));
168 protected void setupNewRepository() throws VcsException {
172 protected void setupExistingRepository() throws VcsException {
178 private void updateSources() throws VcsException {
179 final GitFacade git = myGitFactory.create(myTargetDirectory);
180 boolean branchChanged = false;
182 if (isRegularBranch(myFullBranchName)) {
183 String branchName = getShortBranchName(myFullBranchName);
184 Branches branches = git.listBranches();
185 if (branches.isCurrentBranch(branchName)) {
187 runAndFixIndexErrors(git, new VcsCommand() {
189 public void call() throws VcsException {
190 reset(git).setHard(true).setRevision(myRevision).call();
193 git.setUpstream(branchName, GitUtils.createRemoteRef(myFullBranchName)).call();
195 branchChanged = true;
196 if (!branches.contains(branchName)) {
199 .setStartPoint(GitUtils.createRemoteRef(myFullBranchName))
203 git.updateRef().setRef(myFullBranchName).setRevision(myRevision).call();
204 final String finalBranchName = branchName;
205 runAndFixIndexErrors(git, new VcsCommand() {
207 public void call() throws VcsException {
208 checkout(git).setForce(true).setBranch(finalBranchName).setTimeout(myPluginConfig.getCheckoutIdleTimeoutSeconds()).call();
211 if (branches.contains(branchName)) {
212 git.setUpstream(branchName, GitUtils.createRemoteRef(myFullBranchName)).call();
215 } else if (isTag(myFullBranchName)) {
216 final String shortName = myFullBranchName.substring("refs/tags/".length());
217 runAndFixIndexErrors(git, new VcsCommand() {
219 public void call() throws VcsException {
220 checkout(git).setForce(true).setBranch(shortName).setTimeout(myPluginConfig.getCheckoutIdleTimeoutSeconds()).call();
223 Ref tag = getRef(myTargetDirectory, myFullBranchName);
224 if (tag != null && !tag.getObjectId().name().equals(myRevision)) {
225 runAndFixIndexErrors(git, new VcsCommand() {
227 public void call() throws VcsException {
228 checkout(git).setBranch(myRevision).setForce(true).setTimeout(myPluginConfig.getCheckoutIdleTimeoutSeconds()).call();
232 branchChanged = true;
234 runAndFixIndexErrors(git, new VcsCommand() {
236 public void call() throws VcsException {
237 checkout(git).setForce(true).setBranch(myRevision).setTimeout(myPluginConfig.getCheckoutIdleTimeoutSeconds()).call();
240 branchChanged = true;
243 doClean(branchChanged);
244 if (myRoot.isCheckoutSubmodules()) {
245 checkoutSubmodules(myTargetDirectory);
250 private void runAndFixIndexErrors(@NotNull GitFacade git, @NotNull VcsCommand cmd) throws VcsException {
253 } catch (GitIndexCorruptedException e) {
254 File gitIndex = e.getGitIndex();
255 myLogger.message("Git index '" + gitIndex.getAbsolutePath() + "' is corrupted, remove it and repeat the command");
256 FileUtil.delete(gitIndex);
258 } catch (GitOutdatedIndexException e) {
259 myLogger.message("Refresh outdated git index and repeat the command");
260 updateIndex(git).reallyRefresh(true).quiet(true).call();
262 } catch (Exception e) {
263 if (e instanceof VcsException)
264 throw (VcsException) e;
265 throw new VcsException(e);
271 private UpdateIndexCommand updateIndex(final GitFacade git) {
272 UpdateIndexCommand result = git.updateIndex()
273 .setAuthSettings(myRoot.getAuthSettings())
274 .setUseNativeSsh(myPluginConfig.isUseNativeSSH());
275 configureLFS(result);
281 private ResetCommand reset(final GitFacade git) {
282 ResetCommand result = git.reset()
283 .setAuthSettings(myRoot.getAuthSettings())
284 .setUseNativeSsh(myPluginConfig.isUseNativeSSH());
285 configureLFS(result);
290 private CheckoutCommand checkout(final GitFacade git) {
291 CheckoutCommand result = git.checkout()
292 .setAuthSettings(myRoot.getAuthSettings())
293 .setUseNativeSsh(myPluginConfig.isUseNativeSSH());
294 configureLFS(result);
298 private void checkoutSubmodules(@NotNull final File repositoryDir) throws VcsException {
299 File dotGitModules = new File(repositoryDir, ".gitmodules");
301 Config gitModules = readGitModules(dotGitModules);
302 if (gitModules == null)
305 myLogger.message("Checkout submodules in " + repositoryDir);
306 GitFacade git = myGitFactory.create(repositoryDir);
307 git.submoduleInit().call();
308 git.submoduleSync().call();
310 addSubmoduleUsernames(repositoryDir, gitModules);
312 long start = System.currentTimeMillis();
313 SubmoduleUpdateCommand submoduleUpdate = git.submoduleUpdate()
314 .setAuthSettings(myRoot.getAuthSettings())
315 .setUseNativeSsh(myPluginConfig.isUseNativeSSH())
316 .setTimeout(SILENT_TIMEOUT)
317 .setForce(isForceUpdateSupported());
318 configureLFS(submoduleUpdate);
319 submoduleUpdate.call();
321 if (recursiveSubmoduleCheckout()) {
322 for (String submodulePath : getSubmodulePaths(gitModules)) {
323 checkoutSubmodules(new File(repositoryDir, submodulePath));
326 Loggers.VCS.info("Submodules update in " + repositoryDir.getAbsolutePath() + " is finished in " +
327 (System.currentTimeMillis() - start) + " ms");
329 } catch (IOException e) {
330 Loggers.VCS.error("Submodules checkout failed", e);
331 throw new VcsException("Submodules checkout failed", e);
332 } catch (ConfigInvalidException e) {
333 Loggers.VCS.error("Submodules checkout failed", e);
334 throw new VcsException("Submodules checkout failed", e);
339 private boolean isForceUpdateSupported() {
340 return !GIT_WITH_FORCE_SUBMODULE_UPDATE.isGreaterThan(myPluginConfig.getGitVersion());
344 private void addSubmoduleUsernames(@NotNull File repositoryDir, @NotNull Config gitModules)
345 throws IOException, ConfigInvalidException, VcsException {
346 if (!myPluginConfig.isUseMainRepoUserForSubmodules())
349 Loggers.VCS.info("Update submodules credentials");
351 AuthSettings auth = myRoot.getAuthSettings();
352 final String userName = auth.getUserName();
353 if (userName == null) {
354 Loggers.VCS.info("Username is not specified in the main VCS root settings, skip updating credentials");
358 Repository r = new RepositoryBuilder().setBare().setGitDir(getGitDir(repositoryDir)).build();
360 StoredConfig gitConfig = r.getConfig();
362 Set<String> submodules = gitModules.getSubsections("submodule");
363 if (submodules.isEmpty()) {
364 Loggers.VCS.info("No submodule sections found in " + new File(repositoryDir, ".gitmodules").getCanonicalPath()
365 + ", skip updating credentials");
368 File modulesDir = new File(r.getDirectory(), Constants.MODULES);
369 for (String submoduleName : submodules) {
370 //The 'git submodule sync' command executed before resolves relative submodule urls
371 //from .gitmodules and writes them into .git/config. We should use resolved urls in
372 //order to add parent repository username to submodules with relative urls.
373 String url = gitConfig.getString("submodule", submoduleName, "url");
375 Loggers.VCS.info(".git/config doesn't contain an url for submodule '" + submoduleName + "', use url from .gitmodules");
376 url = gitModules.getString("submodule", submoduleName, "url");
378 Loggers.VCS.info("Update credentials for submodule with url " + url);
379 if (url == null || !isRequireAuth(url)) {
380 Loggers.VCS.info("Url " + url + " does not require authentication, skip updating credentials");
384 URIish uri = new URIish(url);
385 String updatedUrl = uri.setUser(userName).toASCIIString();
386 gitConfig.setString("submodule", submoduleName, "url", updatedUrl);
387 String submodulePath = gitModules.getString("submodule", submoduleName, "path");
388 if (submodulePath != null && myPluginConfig.isUpdateSubmoduleOriginUrl()) {
389 File submoduleDir = new File(modulesDir, submodulePath);
390 if (submoduleDir.isDirectory() && new File(submoduleDir, Constants.CONFIG).isFile())
391 updateOriginUrl(submoduleDir, updatedUrl);
393 Loggers.VCS.debug("Submodule url " + url + " changed to " + updatedUrl);
394 } catch (URISyntaxException e) {
395 Loggers.VCS.warn("Error while parsing an url " + url + ", skip updating submodule credentials", e);
396 } catch (Exception e) {
397 Loggers.VCS.warn("Error while updating the '" + submoduleName + "' submodule url", e);
406 private void updateOriginUrl(@NotNull File repoDir, @NotNull String url) throws IOException {
407 Repository r = new RepositoryBuilder().setBare().setGitDir(repoDir).build();
408 StoredConfig config = r.getConfig();
409 config.setString("remote", "origin", "url", url);
415 private Config readGitModules(@NotNull File dotGitModules) throws IOException, ConfigInvalidException {
416 if (!dotGitModules.exists())
418 String content = FileUtil.readText(dotGitModules);
419 Config config = new Config();
420 config.fromText(content);
425 private boolean isRequireAuth(@NotNull String url) {
427 URIish uri = new URIish(url);
428 String scheme = uri.getScheme();
429 if (scheme == null || "git".equals(scheme)) //no auth for anonymous protocol and for local repositories
431 String user = uri.getUser();
432 if (user != null) //respect a user specified in config
435 } catch (URISyntaxException e) {
441 private Set<String> getSubmodulePaths(@NotNull Config config) {
442 Set<String> paths = new HashSet<String>();
443 Set<String> submodules = config.getSubsections("submodule");
444 for (String submoduleName : submodules) {
445 String submodulePath = config.getString("submodule", submoduleName, "path");
446 paths.add(submodulePath.replaceAll("/", Matcher.quoteReplacement(File.separator)));
451 private boolean recursiveSubmoduleCheckout() {
452 return SubmodulesCheckoutPolicy.CHECKOUT.equals(myRoot.getSubmodulesCheckoutPolicy()) ||
453 SubmodulesCheckoutPolicy.CHECKOUT_IGNORING_ERRORS.equals(myRoot.getSubmodulesCheckoutPolicy());
457 private void doClean(boolean branchChanged) throws VcsException {
458 if (myRoot.getCleanPolicy() == AgentCleanPolicy.ALWAYS ||
459 branchChanged && myRoot.getCleanPolicy() == AgentCleanPolicy.ON_BRANCH_CHANGE) {
460 myLogger.message("Cleaning " + myRoot.getName() + " in " + myTargetDirectory + " the file set " + myRoot.getCleanFilesPolicy());
461 myGitFactory.create(myTargetDirectory).clean().setCleanPolicy(myRoot.getCleanFilesPolicy()).call();
463 if (myRoot.isCheckoutSubmodules())
464 cleanSubmodules(myTargetDirectory);
469 private void cleanSubmodules(@NotNull File repositoryDir) throws VcsException {
470 File dotGitModules = new File(repositoryDir, ".gitmodules");
473 gitModules = readGitModules(dotGitModules);
474 } catch (Exception e) {
475 Loggers.VCS.error("Error while reading " + dotGitModules.getAbsolutePath() + ": " + e.getMessage());
476 throw new VcsException("Error while reading " + dotGitModules.getAbsolutePath(), e);
479 if (gitModules == null)
482 for (String submodulePath : getSubmodulePaths(gitModules)) {
483 File submoduleDir = new File(repositoryDir, submodulePath);
485 myLogger.message("Cleaning files in " + submoduleDir + " the file set " + myRoot.getCleanFilesPolicy());
486 myGitFactory.create(submoduleDir).clean().setCleanPolicy(myRoot.getCleanFilesPolicy()).call();
487 } catch (Exception e) {
488 Loggers.VCS.error("Error while cleaning files in " + submoduleDir.getAbsolutePath(), e);
490 if (recursiveSubmoduleCheckout())
491 cleanSubmodules(submoduleDir);
496 protected void removeUrlSections() throws VcsException {
499 r = new RepositoryBuilder().setWorkTree(myTargetDirectory).build();
500 StoredConfig config = r.getConfig();
501 Set<String> urlSubsections = config.getSubsections("url");
502 for (String subsection : urlSubsections) {
503 config.unsetSection("url", subsection);
506 } catch (IOException e) {
507 String msg = "Error while remove url.* sections";
509 throw new VcsException(msg, e);
517 protected void disableAlternates() {
518 FileUtil.delete(new File(myTargetDirectory, ".git" + File.separator + "objects" + File.separator + "info" + File.separator + "alternates"));
522 private String getRemoteUrl() {
524 return myGitFactory.create(myTargetDirectory).getConfig().setPropertyName("remote.origin.url").call();
525 } catch (VcsException e) {
526 LOG.debug("Failed to read property", e);
533 protected Ref getRef(@NotNull File repositoryDir, @NotNull String ref) {
534 Map<String, Ref> refs = myGitFactory.create(repositoryDir).showRef().setPattern(ref).call().getValidRefs();
535 return refs.isEmpty() ? null : refs.get(ref);
540 * If some git process crashed in this repository earlier it can leave lock files for index.
541 * This method delete such lock file if it exists (with warning message), otherwise git operation will fail.
543 private void removeIndexLock() {
544 File indexLock = new File(myTargetDirectory, ".git" + File.separator + "index.lock");
545 if (indexLock.exists()) {
546 myLogger.warning("The .git/index.lock file exists. This probably means a git process crashed in this repository earlier. Deleting lock file");
547 FileUtil.delete(indexLock);
552 private void doFetch() throws VcsException {
553 boolean outdatedRefsFound = removeOutdatedRefs(myTargetDirectory);
554 ensureCommitLoaded(outdatedRefsFound);
558 protected void ensureCommitLoaded(boolean fetchRequired) throws VcsException {
559 fetchFromOriginalRepository(fetchRequired);
563 protected void fetchFromOriginalRepository(boolean fetchRequired) throws VcsException {
564 if (myPluginConfig.isFetchAllHeads()) {
565 String msg = getForcedHeadsFetchMessage();
567 myLogger.message(msg);
570 if (!myFullBranchName.startsWith("refs/heads/")) {
571 Ref remoteRef = getRef(myTargetDirectory, GitUtils.createRemoteRef(myFullBranchName));
572 if (fetchRequired || remoteRef == null || !myRevision.equals(remoteRef.getObjectId().name()) || !hasRevision(myTargetDirectory, myRevision))
573 fetchDefaultBranch();
576 Ref remoteRef = getRef(myTargetDirectory, GitUtils.createRemoteRef(myFullBranchName));
577 if (!fetchRequired && remoteRef != null && myRevision.equals(remoteRef.getObjectId().name()) && hasRevision(myTargetDirectory, myRevision))
579 myLogger.message("Commit '" + myRevision + "' is not found in local clone. Running 'git fetch'...");
580 fetchDefaultBranch();
581 if (hasRevision(myTargetDirectory, myRevision))
583 myLogger.message("Commit still not found after fetching main branch. Fetching more branches.");
586 if (hasRevision(myTargetDirectory, myRevision))
588 throw new VcsException("Cannot find commit " + myRevision);
592 protected String getForcedHeadsFetchMessage() {
593 return "Forced fetch of all heads (" + PluginConfigImpl.FETCH_ALL_HEADS + "=" + myBuild.getSharedConfigParameters().get(PluginConfigImpl.FETCH_ALL_HEADS) + ")";
597 private void fetchDefaultBranch() throws VcsException {
598 fetch(myTargetDirectory, getRefspecForFetch(), false);
601 private String getRefspecForFetch() {
602 return "+" + myFullBranchName + ":" + GitUtils.createRemoteRef(myFullBranchName);
605 private void fetchAllBranches() throws VcsException {
606 fetch(myTargetDirectory, "+refs/heads/*:refs/remotes/origin/*", false);
609 protected boolean hasRevision(@NotNull File repositoryDir, @NotNull String revision) {
610 return getRevision(repositoryDir, revision) != null;
613 private String getRevision(@NotNull File repositoryDir, @NotNull String revision) {
614 return myGitFactory.create(repositoryDir).log()
616 .setPrettyFormat("%H%x20%s")
617 .setStartPoint(revision)
621 protected void fetch(@NotNull File repositoryDir, @NotNull String refspec, boolean shallowClone) throws VcsException {
622 boolean silent = isSilentFetch();
623 int timeout = getTimeout(silent);
626 getFetch(repositoryDir, refspec, shallowClone, silent, timeout).call();
627 } catch (GitIndexCorruptedException e) {
628 File gitIndex = e.getGitIndex();
629 myLogger.message("Git index '" + gitIndex.getAbsolutePath() + "' is corrupted, remove it and repeat git fetch");
630 FileUtil.delete(gitIndex);
631 getFetch(repositoryDir, refspec, shallowClone, silent, timeout).call();
632 } catch (GitExecTimeout e) {
634 myLogger.error("No output from git during " + timeout + " seconds. Try increasing idle timeout by setting parameter '"
635 + PluginConfigImpl.IDLE_TIMEOUT +
636 "' either in build or in agent configuration.");
643 private FetchCommand getFetch(@NotNull File repositoryDir, @NotNull String refspec, boolean shallowClone, boolean silent, int timeout) {
644 FetchCommand result = myGitFactory.create(repositoryDir).fetch()
645 .setAuthSettings(myRoot.getAuthSettings())
646 .setUseNativeSsh(myPluginConfig.isUseNativeSSH())
649 .setFetchTags(myPluginConfig.isFetchTags());
652 result.setQuite(true);
654 result.setShowProgress(true);
662 protected void removeRefLocks(@NotNull File dotGit) {
663 File refs = new File(dotGit, "refs");
664 if (!refs.isDirectory())
666 Collection<File> locks = FileUtil.findFiles(new FileFilter() {
667 public boolean accept(File f) {
668 return f.isFile() && f.getName().endsWith(".lock");
671 for (File lock : locks) {
672 LOG.info("Remove a lock file " + lock.getAbsolutePath());
673 FileUtil.delete(lock);
677 private boolean isSilentFetch() {
678 GitVersion version = myPluginConfig.getGitVersion();
679 return version.isLessThan(GIT_WITH_PROGRESS_VERSION);
682 private int getTimeout(boolean silentFetch) {
684 return SILENT_TIMEOUT;
686 return myPluginConfig.getIdleTimeoutSeconds();
690 private void checkAuthMethodIsSupported() throws VcsException {
691 checkAuthMethodIsSupported(myRoot, myPluginConfig);
695 static void checkAuthMethodIsSupported(@NotNull GitVcsRoot root, @NotNull AgentPluginConfig config) throws VcsException {
696 if ("git".equals(root.getRepositoryFetchURL().getScheme()))
697 return;//anonymous protocol, don't check anything
698 AuthSettings authSettings = root.getAuthSettings();
699 switch (authSettings.getAuthMethod()) {
701 if ("http".equals(root.getRepositoryFetchURL().getScheme()) ||
702 "https".equals(root.getRepositoryFetchURL().getScheme())) {
703 GitVersion actualVersion = config.getGitVersion();
704 GitVersion requiredVersion = getMinVersionForHttpAuth();
705 if (actualVersion.isLessThan(requiredVersion)) {
706 throw new VcsException("Password authentication requires git " + requiredVersion +
707 ", found git version is " + actualVersion +
708 ". Upgrade git or use different authentication method.");
711 throw new VcsException("TeamCity doesn't support authentication method '" +
712 root.getAuthSettings().getAuthMethod().uiName() +
713 "' with agent checkout and non-http protocols. Please use different authentication method.");
716 case PRIVATE_KEY_FILE:
717 throw new VcsException("TeamCity doesn't support authentication method '" +
718 root.getAuthSettings().getAuthMethod().uiName() +
719 "' with agent checkout. Please use different authentication method.");
724 private static GitVersion getMinVersionForHttpAuth() {
725 //core.askpass parameter was added in 1.7.1, but
726 //experiments show that it works only in 1.7.3 on linux
727 //and msysgit 1.7.3.1-preview20101002.
728 return new GitVersion(1, 7, 3);
732 * Clean and init directory and configure remote origin
734 * @throws VcsException if there are problems with initializing the directory
736 private void initDirectory() throws VcsException {
737 BuildDirectoryCleanerCallback c = new BuildDirectoryCleanerCallback(myLogger, LOG);
738 myDirectoryCleaner.cleanFolder(myTargetDirectory, c);
739 //noinspection ResultOfMethodCallIgnored
740 myTargetDirectory.mkdirs();
741 if (c.isHasErrors()) {
742 throw new VcsException("Unable to clean directory " + myTargetDirectory + " for VCS root " + myRoot.getName());
744 myLogger.message("The .git directory is missing in '" + myTargetDirectory + "'. Running 'git init'...");
745 myGitFactory.create(myTargetDirectory).init().call();
747 configureRemoteUrl(new File(myTargetDirectory, ".git"));
749 URIish fetchUrl = myRoot.getRepositoryFetchURL();
750 URIish url = myRoot.getRepositoryPushURL();
751 String pushUrl = url == null ? null : url.toString();
752 if (pushUrl != null && !pushUrl.equals(fetchUrl.toString())) {
753 myGitFactory.create(myTargetDirectory).setConfig().setPropertyName("remote.origin.pushurl").setValue(pushUrl).call();
755 setupNewRepository();
756 configureSparseCheckout();
760 void configureRemoteUrl(@NotNull File gitDir) throws VcsException {
761 RemoteRepositoryConfigurator cfg = new RemoteRepositoryConfigurator();
762 cfg.setGitDir(gitDir);
763 cfg.setExcludeUsernameFromHttpUrls(myPluginConfig.isExcludeUsernameFromHttpUrl() && !myPluginConfig.getGitVersion().isLessThan(UpdaterImpl.CREDENTIALS_SECTION_VERSION));
764 cfg.configure(myRoot);
768 private void configureSparseCheckout() throws VcsException {
769 if (myCheckoutMode == CheckoutMode.SPARSE_CHECKOUT) {
770 setupSparseCheckout();
772 myGitFactory.create(myTargetDirectory).setConfig().setPropertyName("core.sparseCheckout").setValue("false").call();
776 private void setupSparseCheckout() throws VcsException {
777 myGitFactory.create(myTargetDirectory).setConfig().setPropertyName("core.sparseCheckout").setValue("true").call();
778 File sparseCheckout = new File(myTargetDirectory, ".git/info/sparse-checkout");
779 boolean hasIncludeRules = false;
780 StringBuilder sparseCheckoutContent = new StringBuilder();
781 for (IncludeRule rule : myRules.getIncludeRules()) {
782 if (isEmpty(rule.getFrom())) {
783 sparseCheckoutContent.append("/*\n");
785 sparseCheckoutContent.append("/").append(rule.getFrom()).append("\n");
787 hasIncludeRules = true;
789 if (!hasIncludeRules) {
790 sparseCheckoutContent.append("/*\n");
792 for (FileRule rule : myRules.getExcludeRules()) {
793 sparseCheckoutContent.append("!/").append(rule.getFrom()).append("\n");
796 FileUtil.writeFileAndReportErrors(sparseCheckout, sparseCheckoutContent.toString());
797 } catch (IOException e) {
798 LOG.warn("Error while writing sparse checkout config, disable sparse checkout", e);
799 myGitFactory.create(myTargetDirectory).setConfig().setPropertyName("core.sparseCheckout").setValue("false").call();
804 private void validateUrls() {
805 URIish fetch = myRoot.getRepositoryFetchURL();
806 if (isAnonymousGitWithUsername(fetch))
807 LOG.warn("Fetch URL '" + fetch.toString() + "' for root " + myRoot.getName() + " uses an anonymous git protocol and contains a username, fetch will probably fail");
808 URIish push = myRoot.getRepositoryPushURL();
809 if (!fetch.equals(push) && isAnonymousGitWithUsername(push))
810 LOG.warn("Push URL '" + push.toString() + "'for root " + myRoot.getName() + " uses an anonymous git protocol and contains a username, push will probably fail");
814 protected boolean removeOutdatedRefs(@NotNull File workingDir) throws VcsException {
815 boolean outdatedRefsRemoved = false;
816 GitFacade git = myGitFactory.create(workingDir);
817 ShowRefResult showRefResult = git.showRef().call();
818 Refs localRefs = new Refs(showRefResult.getValidRefs());
819 if (localRefs.isEmpty() && showRefResult.getInvalidRefs().isEmpty())
821 for (String invalidRef : showRefResult.getInvalidRefs()) {
822 git.updateRef().setRef(invalidRef).delete().call();
823 outdatedRefsRemoved = true;
825 final Refs remoteRefs;
827 remoteRefs = getRemoteRefs(workingDir);
828 } catch (VcsException e) {
829 if (CommandUtil.isCanceledError(e))
831 String msg = "Failed to list remote repository refs, outdated local refs will not be cleaned";
833 myLogger.warning(msg);
836 //We remove both outdated local refs (e.g. refs/heads/topic) and outdated remote
837 //tracking branches (refs/remote/origin/topic), while git remote origin prune
838 //removes only the latter. We need that because in some cases git cannot handle
839 //rename of the branch (TW-28735).
840 for (Ref localRef : localRefs.list()) {
841 Ref correspondingRemoteRef = createCorrespondingRemoteRef(localRef);
842 if (remoteRefs.isOutdated(correspondingRemoteRef)) {
843 git.updateRef().setRef(localRef.getName()).delete().call();
844 outdatedRefsRemoved = true;
847 return outdatedRefsRemoved;
852 private Refs getRemoteRefs(@NotNull File workingDir) throws VcsException {
853 if (myRemoteRefs != null)
855 GitFacade git = myGitFactory.create(workingDir);
856 myRemoteRefs = new Refs(git.lsRemote().setAuthSettings(myRoot.getAuthSettings())
857 .setUseNativeSsh(myPluginConfig.isUseNativeSSH())
863 private boolean isRemoteTrackingBranch(@NotNull Ref localRef) {
864 return localRef.getName().startsWith("refs/remotes/origin");
868 private Ref createCorrespondingRemoteRef(@NotNull Ref localRef) {
869 if (!isRemoteTrackingBranch(localRef))
871 return new RefImpl("refs/heads" + localRef.getName().substring("refs/remotes/origin".length()),
872 localRef.getObjectId().name());
876 private void configureLFS(@NotNull BaseCommand command) {
877 if (!myPluginConfig.isProvideCredHelper())
879 Trinity<String, String, String> lfsAuth = getLfsAuth();
882 File credentialsHelper = null;
884 ScriptGen scriptGen = myGitFactory.create(new File(".")).getScriptGen();
885 final File credHelper = scriptGen.generateCredentialsHelper();
886 credentialsHelper = credHelper;
887 if (!myPluginConfig.getGitVersion().isLessThan(UpdaterImpl.EMPTY_CRED_HELPER)) {
888 //Specify an empty helper if it is supported in order to disable
889 //helpers in system-global-local chain. If empty helper is not supported,
890 //then the only workaround is to disable helpers manually in config files.
891 command.addConfig("credential.helper", "");
893 String path = credHelper.getCanonicalPath();
894 path = path.replaceAll("\\\\", "/");
895 command.addConfig("credential.helper", path);
896 CredentialsHelperConfig config = new CredentialsHelperConfig();
897 config.addCredentials(lfsAuth.first, lfsAuth.second, lfsAuth.third);
898 config.setMatchAllUrls(myPluginConfig.isCredHelperMatchesAllUrls());
899 for (Map.Entry<String, String> e : config.getEnv().entrySet()) {
900 command.setEnv(e.getKey(), e.getValue());
902 if (myPluginConfig.isCleanCredHelperScript()) {
903 command.addPostAction(new Runnable() {
906 FileUtil.delete(credHelper);
910 } catch (Exception e) {
911 if (credentialsHelper != null)
912 FileUtil.delete(credentialsHelper);
917 //returns (url, name, pass) for lfs or null if no authentication is required or
918 //root doesn't use http(s)
920 private Trinity<String, String, String> getLfsAuth() {
922 URIish uri = new URIish(myRoot.getRepositoryFetchURL().toString());
923 String scheme = uri.getScheme();
924 if (myRoot.getAuthSettings().getAuthMethod() == AuthenticationMethod.PASSWORD &&
925 ("http".equals(scheme) || "https".equals(scheme))) {
926 String lfsUrl = uri.setPass("").setUser("").toASCIIString();
927 if (lfsUrl.endsWith(".git")) {
928 lfsUrl += "/info/lfs";
930 lfsUrl += lfsUrl.endsWith("/") ? ".git/info/lfs" : "/.git/info/lfs";
932 return Trinity.create(lfsUrl, myRoot.getAuthSettings().getUserName(), myRoot.getAuthSettings().getPassword());
934 } catch (Exception e) {
935 LOG.debug("Cannot get lfs auth config", e);
941 private interface VcsCommand {
942 void call() throws VcsException;
947 * Removes .idx files which don't have a corresponding .pack file
948 * @param ditGitDir git dir
950 void removeOrphanedIdxFiles(@NotNull File ditGitDir) {
951 if ("false".equals(myBuild.getSharedConfigParameters().get("teamcity.git.removeOrphanedIdxFiles"))) {
952 //looks like this logic is always needed, if no problems will be reported we can drop the option
955 File packDir = new File(new File(ditGitDir, "objects"), "pack");
956 File[] files = packDir.listFiles();
957 if (files == null || files.length == 0)
960 Set<String> packs = new HashSet<String>();
961 for (File f : files) {
962 String name = f.getName();
963 if (name.endsWith(".pack")) {
964 packs.add(name.substring(0, name.length() - 5));
968 for (File f : files) {
969 String name = f.getName();
970 if (name.endsWith(".idx")) {
971 if (!packs.contains(name.substring(0, name.length() - 4)))