Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Octocat-spinner-32-eaf2f5

Cannot retrieve contributors at this time

file 311 lines (271 sloc) 14.15 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
package hudson.plugins.git.util;

import hudson.Extension;
import hudson.EnvVars;
import hudson.model.TaskListener;
import hudson.plugins.git.*;
import hudson.remoting.VirtualChannel;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.RemoteConfig;
import org.jenkinsci.plugins.gitclient.GitClient;
import org.jenkinsci.plugins.gitclient.RepositoryCallback;
import org.kohsuke.stapler.DataBoundConstructor;

import java.io.IOException;
import java.text.MessageFormat;
import java.util.*;

import static java.util.Collections.emptyList;

public class DefaultBuildChooser extends BuildChooser {

    /* Ignore symbolic default branch ref. */
    private static final BranchSpec HEAD = new BranchSpec("*/HEAD");

    @DataBoundConstructor
    public DefaultBuildChooser() {
    }

    /**
* Determines which Revisions to build.
*
* If only one branch is chosen and only one repository is listed, then
* just attempt to find the latest revision number for the chosen branch.
*
* If multiple branches are selected or the branches include wildcards, then
* use the advanced usecase as defined in the getAdvancedCandidateRevisons
* method.
*
* @throws IOException
* @throws GitException
*/
    @Override
    public Collection<Revision> getCandidateRevisions(boolean isPollCall, String branchSpec,
                                                      GitClient git, TaskListener listener, BuildData data, BuildChooserContext context)
            throws GitException, IOException, InterruptedException {

        verbose(listener,"getCandidateRevisions({0},{1},,,{2}) considering branches to build",isPollCall,branchSpec,data);

        // if the branch name contains more wildcards then the simple usecase
        // does not apply and we need to skip to the advanced usecase
        if (branchSpec == null || branchSpec.contains("*"))
            return getAdvancedCandidateRevisions(isPollCall,listener,new GitUtils(listener,git),data, context);

        // check if we're trying to build a specific commit
        // this only makes sense for a build, there is no
        // reason to poll for a commit
        if (!isPollCall && branchSpec.matches("[0-9a-f]{6,40}")) {
            try {
                ObjectId sha1 = git.revParse(branchSpec);
                Revision revision = new Revision(sha1);
                revision.getBranches().add(new Branch("detached", sha1));
                verbose(listener,"Will build the detached SHA1 {0}",sha1);
                return Collections.singletonList(revision);
            } catch (GitException e) {
                // revision does not exist, may still be a branch
                // for example a branch called "badface" would show up here
                verbose(listener, "Not a valid SHA1 {0}", branchSpec);
            }
        }

        Collection<Revision> revisions = new ArrayList<Revision>();

        // if it doesn't contain '/' then it could be an unqualified branch
        if (!branchSpec.contains("/")) {

            // <tt>BRANCH</tt> is recognized as a shorthand of <tt>*/BRANCH</tt>
            // so check all remotes to fully qualify this branch spec
            for (RemoteConfig config : gitSCM.getRepositories()) {
                String repository = config.getName();
                String fqbn = repository + "/" + branchSpec;
                verbose(listener, "Qualifying {0} as a branch in repository {1} -> {2}", branchSpec, repository, fqbn);
                revisions.addAll(getHeadRevision(isPollCall, fqbn, git, listener, data));
            }
        } else {
            // either the branch is qualified (first part should match a valid remote)
            // or it is still unqualified, but the branch name contains a '/'
            List<String> possibleQualifiedBranches = new ArrayList<String>();
            for (RemoteConfig config : gitSCM.getRepositories()) {
                String repository = config.getName();
                String fqbn;
                if (branchSpec.startsWith(repository + "/")) {
                    fqbn = "refs/remotes/" + branchSpec;
                } else if(branchSpec.startsWith("remotes/" + repository + "/")) {
                    fqbn = "refs/" + branchSpec;
                } else if(branchSpec.startsWith("refs/heads/")) {
                    fqbn = "refs/remotes/" + repository + "/" + branchSpec.substring("refs/heads/".length());
                } else {
                    //Try branchSpec as it is - e.g. "refs/tags/mytag"
                    fqbn = branchSpec;
                }
                verbose(listener, "Qualifying {0} as a branch in repository {1} -> {2}", branchSpec, repository, fqbn);
                possibleQualifiedBranches.add(fqbn);

                //Check if exact branch name <branchSpec> existss
                fqbn = "refs/remotes/" + repository + "/" + branchSpec;
                verbose(listener, "Qualifying {0} as a branch in repository {1} -> {2}", branchSpec, repository, fqbn);
                possibleQualifiedBranches.add(fqbn);
            }
            for (String fqbn : possibleQualifiedBranches) {
              revisions.addAll(getHeadRevision(isPollCall, fqbn, git, listener, data));
            }
        }

        if (revisions.isEmpty()) {
            // the 'branch' could actually be a non branch reference (for example a tag or a gerrit change)

            revisions = getHeadRevision(isPollCall, branchSpec, git, listener, data);
            if (!revisions.isEmpty()) {
                verbose(listener, "{0} seems to be a non-branch reference (tag?)");
            }
        }
        
        return revisions;
    }

    private Collection<Revision> getHeadRevision(boolean isPollCall, String singleBranch, GitClient git, TaskListener listener, BuildData data) throws InterruptedException {
        try {
            ObjectId sha1 = git.revParse(singleBranch);
            verbose(listener, "rev-parse {0} -> {1}", singleBranch, sha1);

            // if polling for changes don't select something that has
            // already been built as a build candidate
            if (isPollCall && data.hasBeenBuilt(sha1)) {
                verbose(listener, "{0} has already been built", sha1);
                return emptyList();
            }

            verbose(listener, "Found a new commit {0} to be built on {1}", sha1, singleBranch);
            Revision revision = new Revision(sha1);
            revision.getBranches().add(new Branch(singleBranch, sha1));
            return Collections.singletonList(revision);
            /*
// calculate the revisions that are new compared to the last build
List<Revision> candidateRevs = new ArrayList<Revision>();
List<ObjectId> allRevs = git.revListAll(); // index 0 contains the newest revision
if (data != null && allRevs != null) {
Revision lastBuiltRev = data.getLastBuiltRevision();
if (lastBuiltRev == null) {
return Collections.singletonList(objectId2Revision(singleBranch, sha1));
}
int indexOfLastBuildRev = allRevs.indexOf(lastBuiltRev.getSha1());
if (indexOfLastBuildRev == -1) {
// mhmmm ... can happen when branches are switched.
return Collections.singletonList(objectId2Revision(singleBranch, sha1));
}
List<ObjectId> newRevisionsSinceLastBuild = allRevs.subList(0, indexOfLastBuildRev);
// translate list of ObjectIds into list of Revisions
for (ObjectId objectId : newRevisionsSinceLastBuild) {
candidateRevs.add(objectId2Revision(singleBranch, objectId));
}
}
if (candidateRevs.isEmpty()) {
return Collections.singletonList(objectId2Revision(singleBranch, sha1));
}
return candidateRevs;
*/
        } catch (GitException e) {
            // branch does not exist, there is nothing to build
            verbose(listener, "Failed to rev-parse: {0}", singleBranch);
            return emptyList();
        }
    }

    private Revision objectId2Revision(String singleBranch, ObjectId sha1) {
        Revision revision = new Revision(sha1);
        revision.getBranches().add(new Branch(singleBranch, sha1));
        return revision;
    }

    /**
* In order to determine which Revisions to build.
*
* Does the following :
* 1. Find all the branch revisions
* 2. Filter out branches that we don't care about from the revisions.
* Any Revisions with no interesting branches are dropped.
* 3. Get rid of any revisions that are wholly subsumed by another
* revision we're considering.
* 4. Get rid of any revisions that we've already built.
* 5. Sort revisions from old to new.
*
* NB: Alternate BuildChooser implementations are possible - this
* may be beneficial if "only 1" branch is to be built, as much of
* this work is irrelevant in that usecase.
* @throws IOException
* @throws GitException
*/
    private List<Revision> getAdvancedCandidateRevisions(boolean isPollCall, TaskListener listener, GitUtils utils, BuildData data, BuildChooserContext context) throws GitException, IOException, InterruptedException {

        EnvVars env = context.getEnvironment();

        // 1. Get all the (branch) revisions that exist
        List<Revision> revs = new ArrayList<Revision>(utils.getAllBranchRevisions());
        verbose(listener, "Starting with all the branches: {0}", revs);

        // 2. Filter out any revisions that don't contain any branches that we
        // actually care about (spec)
        for (Iterator<Revision> i = revs.iterator(); i.hasNext();) {
            Revision r = i.next();

            // filter out uninteresting branches
            for (Iterator<Branch> j = r.getBranches().iterator(); j.hasNext();) {
                Branch b = j.next();
                boolean keep = false;
                for (BranchSpec bspec : gitSCM.getBranches()) {
                    if (bspec.matches(b.getName(), env)) {
                        keep = true;
                        break;
                    }
                }

                if (!keep) {
                    verbose(listener, "Ignoring {0} because it doesn''t match branch specifier", b);
                    j.remove();
                }
            }
            
            // filter out HEAD ref if it's not the only ref
            if (r.getBranches().size() > 1) {
                for (Iterator<Branch> j = r.getBranches().iterator(); j.hasNext();) {
                    Branch b = j.next();
                    if (HEAD.matches(b.getName(), env)) {
                     verbose(listener, "Ignoring {0} because there''s named branch for this revision", b.getName());
                     j.remove();
                    }
                }
            }

            if (r.getBranches().size() == 0) {
                verbose(listener, "Ignoring {0} because we don''t care about any of the branches that point to it", r);
                i.remove();
            }
        }

        verbose(listener, "After branch filtering: {0}", revs);

        // 3. We only want 'tip' revisions
        revs = utils.filterTipBranches(revs);
        verbose(listener, "After non-tip filtering: {0}", revs);

        // 4. Finally, remove any revisions that have already been built.
        verbose(listener, "Removing what''s already been built: {0}", data.getBuildsByBranchName());
        Revision lastBuiltRevision = data.getLastBuiltRevision();
        for (Iterator<Revision> i = revs.iterator(); i.hasNext();) {
            Revision r = i.next();

            if (data.hasBeenBuilt(r.getSha1())) {
                i.remove();
                
                // keep track of new branches pointing to the last built revision
                if (lastBuiltRevision != null && lastBuiltRevision.getSha1().equals(r.getSha1())) {
                 lastBuiltRevision = r;
                }
            }
        }
        verbose(listener, "After filtering out what''s already been built: {0}", revs);

        // if we're trying to run a build (not an SCM poll) and nothing new
        // was found then just run the last build again but ensure that the branch list
        // is ordered according to the configuration. Sorting the branch list ensures
        // a deterministic value for GIT_BRANCH and allows a git-flow style workflow
        // with fast-forward merges between branches
        if (!isPollCall && revs.isEmpty() && lastBuiltRevision != null) {
            verbose(listener, "Nothing seems worth building, so falling back to the previously built revision: {0}", data.getLastBuiltRevision());
            return Collections.singletonList(utils.sortBranchesForRevision(lastBuiltRevision, gitSCM.getBranches(), env));
        }

        // 5. sort them by the date of commit, old to new
        // this ensures the fairness in scheduling.
        final List<Revision> in = revs;
        return utils.git.withRepository(new RepositoryCallback<List<Revision>>() {
            public List<Revision> invoke(Repository repo, VirtualChannel channel) throws IOException, InterruptedException {
                Collections.sort(in,new CommitTimeComparator(repo));
                return in;
            }
        });
    }

    /**
* Write the message to the listener only when the verbose mode is on.
*/
    private void verbose(TaskListener listener, String format, Object... args) {
        if (GitSCM.VERBOSE)
            listener.getLogger().println(MessageFormat.format(format,args));
    }

    @Extension
    public static final class DescriptorImpl extends BuildChooserDescriptor {
        @Override
        public String getDisplayName() {
            return Messages.BuildChooser_Default();
        }

        @Override
        public String getLegacyId() {
            return "Default";
        }
    }
}
Something went wrong with that request. Please try again.