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