001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.scm.provider.git.jgit.command.checkout;
020
021import java.io.File;
022import java.io.IOException;
023import java.util.ArrayList;
024import java.util.List;
025import java.util.Set;
026import java.util.function.BiFunction;
027
028import org.apache.commons.lang3.StringUtils;
029import org.apache.maven.scm.ScmException;
030import org.apache.maven.scm.ScmFile;
031import org.apache.maven.scm.ScmFileSet;
032import org.apache.maven.scm.ScmFileStatus;
033import org.apache.maven.scm.ScmTag;
034import org.apache.maven.scm.ScmVersion;
035import org.apache.maven.scm.command.checkout.AbstractCheckOutCommand;
036import org.apache.maven.scm.command.checkout.CheckOutScmResult;
037import org.apache.maven.scm.command.remoteinfo.RemoteInfoScmResult;
038import org.apache.maven.scm.provider.ScmProviderRepository;
039import org.apache.maven.scm.provider.git.command.GitCommand;
040import org.apache.maven.scm.provider.git.jgit.command.CustomizableSshSessionFactoryCommand;
041import org.apache.maven.scm.provider.git.jgit.command.JGitTransportConfigCallback;
042import org.apache.maven.scm.provider.git.jgit.command.JGitUtils;
043import org.apache.maven.scm.provider.git.jgit.command.ScmProviderAwareSshdSessionFactory;
044import org.apache.maven.scm.provider.git.jgit.command.branch.JGitBranchCommand;
045import org.apache.maven.scm.provider.git.jgit.command.remoteinfo.JGitRemoteInfoCommand;
046import org.apache.maven.scm.provider.git.repository.GitScmProviderRepository;
047import org.eclipse.jgit.api.CloneCommand;
048import org.eclipse.jgit.api.FetchCommand;
049import org.eclipse.jgit.api.Git;
050import org.eclipse.jgit.api.PullCommand;
051import org.eclipse.jgit.api.TransportConfigCallback;
052import org.eclipse.jgit.api.errors.GitAPIException;
053import org.eclipse.jgit.lib.Constants;
054import org.eclipse.jgit.lib.ProgressMonitor;
055import org.eclipse.jgit.revwalk.RevCommit;
056import org.eclipse.jgit.revwalk.RevWalk;
057import org.eclipse.jgit.storage.file.WindowCacheConfig;
058import org.eclipse.jgit.transport.CredentialsProvider;
059import org.eclipse.jgit.treewalk.TreeWalk;
060import org.slf4j.Logger;
061
062/**
063 * @author <a href="mailto:struberg@yahoo.de">Mark Struberg</a>
064 * @author Dominik Bartholdi (imod)
065 * @since 1.9
066 */
067public class JGitCheckOutCommand extends AbstractCheckOutCommand
068        implements GitCommand, CustomizableSshSessionFactoryCommand {
069    private BiFunction<GitScmProviderRepository, Logger, ScmProviderAwareSshdSessionFactory> sshSessionFactorySupplier;
070
071    public JGitCheckOutCommand() {
072        sshSessionFactorySupplier = ScmProviderAwareSshdSessionFactory::new;
073    }
074
075    @Override
076    public void setSshSessionFactorySupplier(
077            BiFunction<GitScmProviderRepository, Logger, ScmProviderAwareSshdSessionFactory>
078                    sshSessionFactorySupplier) {
079        this.sshSessionFactorySupplier = sshSessionFactorySupplier;
080    }
081
082    /**
083     * For git, the given repository is a remote one. We have to clone it first if the working directory does not
084     * contain a git repo yet, otherwise we have to git-pull it.
085     * <p>
086     * {@inheritDoc}
087     */
088    protected CheckOutScmResult executeCheckOutCommand(
089            ScmProviderRepository repo, ScmFileSet fileSet, ScmVersion version, boolean recursive, boolean shallow)
090            throws ScmException {
091        GitScmProviderRepository repository = (GitScmProviderRepository) repo;
092
093        if (GitScmProviderRepository.PROTOCOL_FILE.equals(
094                        repository.getFetchInfo().getProtocol())
095                && repository
096                                .getFetchInfo()
097                                .getPath()
098                                .indexOf(fileSet.getBasedir().getPath())
099                        >= 0) {
100            throw new ScmException("remote repository must not be the working directory");
101        }
102
103        Git git = null;
104        try {
105
106            ProgressMonitor monitor = JGitUtils.getMonitor();
107
108            String branch = version != null ? version.getName() : null;
109
110            if (StringUtils.isBlank(branch)) {
111                branch = Constants.MASTER;
112            }
113
114            TransportConfigCallback transportConfigCallback = new JGitTransportConfigCallback(
115                    sshSessionFactorySupplier.apply((GitScmProviderRepository) repo, logger));
116
117            logger.debug("try checkout of branch: " + branch);
118
119            if (!fileSet.getBasedir().exists() || !(new File(fileSet.getBasedir(), ".git").exists())) {
120                if (fileSet.getBasedir().exists()) {
121                    // git refuses to clone otherwise
122                    fileSet.getBasedir().delete();
123                }
124
125                // FIXME only if windauze
126                WindowCacheConfig cfg = new WindowCacheConfig();
127                cfg.setPackedGitMMAP(false);
128                cfg.install();
129
130                // no git repo seems to exist, let's clone the original repo
131                CredentialsProvider credentials = JGitUtils.getCredentials((GitScmProviderRepository) repo);
132                logger.info("cloning [" + branch + "] to " + fileSet.getBasedir());
133                CloneCommand command = Git.cloneRepository().setURI(repository.getFetchUrl());
134
135                command.setCredentialsProvider(credentials).setBranch(branch).setDirectory(fileSet.getBasedir());
136
137                command.setTransportConfigCallback(transportConfigCallback);
138
139                command.setProgressMonitor(monitor);
140                git = command.call();
141            }
142
143            JGitRemoteInfoCommand remoteInfoCommand = new JGitRemoteInfoCommand();
144            remoteInfoCommand.setSshSessionFactorySupplier(sshSessionFactorySupplier);
145            RemoteInfoScmResult result = remoteInfoCommand.executeRemoteInfoCommand(repository, fileSet, null);
146
147            if (git == null) {
148                // deliberately not using JGitUtils.openRepo(), the caller told us exactly where to checkout
149                git = Git.open(fileSet.getBasedir());
150            }
151
152            if (fileSet.getBasedir().exists()
153                    && new File(fileSet.getBasedir(), ".git").exists()
154                    && result.getBranches().size() > 0) {
155                // git repo exists, so we must git-pull the changes
156                CredentialsProvider credentials = JGitUtils.prepareSession(git, repository);
157
158                if (version != null && StringUtils.isNotEmpty(version.getName()) && (version instanceof ScmTag)) {
159                    // A tag will not be pulled but we only fetch all the commits from the upstream repo
160                    // This is done because checking out a tag might not happen on the current branch
161                    // but create a 'detached HEAD'.
162                    // In fact, a tag in git may be in multiple branches. This occurs if
163                    // you create a branch after the tag has been created
164                    logger.debug("fetch...");
165                    FetchCommand command =
166                            git.fetch().setCredentialsProvider(credentials).setProgressMonitor(monitor);
167                    command.setTransportConfigCallback(transportConfigCallback);
168                    command.call();
169
170                } else {
171                    logger.debug("pull...");
172                    PullCommand command =
173                            git.pull().setCredentialsProvider(credentials).setProgressMonitor(monitor);
174                    command.setTransportConfigCallback(transportConfigCallback);
175                    command.call();
176                }
177            }
178
179            Set<String> localBranchNames = JGitBranchCommand.getShortLocalBranchNames(git);
180            if (version instanceof ScmTag) {
181                logger.info("checkout tag [" + branch + "] at " + fileSet.getBasedir());
182                git.checkout().setName(branch).call();
183            } else if (localBranchNames.contains(branch)) {
184                logger.info("checkout [" + branch + "] at " + fileSet.getBasedir());
185                git.checkout().setName(branch).call();
186            } else {
187                logger.info("checkout remote branch [" + branch + "] at " + fileSet.getBasedir());
188                git.checkout()
189                        .setName(branch)
190                        .setCreateBranch(true)
191                        .setStartPoint(Constants.DEFAULT_REMOTE_NAME + "/" + branch)
192                        .call();
193            }
194
195            RevWalk revWalk = new RevWalk(git.getRepository());
196            RevCommit commit = revWalk.parseCommit(git.getRepository().resolve(Constants.HEAD));
197            revWalk.close();
198
199            try (TreeWalk walk = new TreeWalk(git.getRepository())) {
200                walk.reset(); // drop the first empty tree, which we do not need here
201                walk.setRecursive(true);
202                walk.addTree(commit.getTree());
203
204                List<ScmFile> listedFiles = new ArrayList<>();
205                while (walk.next()) {
206                    listedFiles.add(new ScmFile(walk.getPathString(), ScmFileStatus.CHECKED_OUT));
207                }
208                logger.debug("current branch: " + git.getRepository().getBranch());
209
210                return new CheckOutScmResult("checkout via JGit", listedFiles);
211            }
212        } catch (RuntimeException | IOException | GitAPIException e) {
213            throw new ScmException("JGit checkout failure!", e);
214        } finally {
215            JGitUtils.closeRepo(git);
216        }
217    }
218}