diff --git a/.gitignore b/.gitignore index 88b008a986..e7e1a8903e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ target/ work/ +.@tmp/ .project .classpath @@ -21,4 +22,7 @@ tags # emacs backup files *~ -/nbproject/ \ No newline at end of file +/nbproject/ + +# Mac OSX +.DS_Store \ No newline at end of file diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml new file mode 100644 index 0000000000..94863e605b --- /dev/null +++ b/.mvn/extensions.xml @@ -0,0 +1,7 @@ + + + io.jenkins.tools.incrementals + git-changelist-maven-extension + 1.0-beta-7 + + diff --git a/.mvn/maven.config b/.mvn/maven.config new file mode 100644 index 0000000000..2a0299c486 --- /dev/null +++ b/.mvn/maven.config @@ -0,0 +1,2 @@ +-Pconsume-incrementals +-Pmight-produce-incrementals diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 202834bf1f..665d30307d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ or can be submitted directly if you have commit permission to the git-client-plugin repository. Pull requests are evaluated by the -[Cloudbees Jenkins job](https://jenkins.ci.cloudbees.com/job/plugins/job/git-client-plugin/). +[ci.jenkins.io Jenkins job](https://ci.jenkins.io/job/Plugins/job/git-client-plugin/). You should receive e-mail with the results of the evaluation. Before submitting your change, please assure that you've added tests @@ -27,10 +27,12 @@ assure that you haven't introduced new findbugs warnings. # Code Style Guidelines -## Indentation +Use the [Jenkins SCM API coding style guide](https://github.com/jenkinsci/scm-api-plugin/blob/master/CONTRIBUTING.md#code-style-guidelines) for new code. -* Code formatting in the git client plugin varies between files. Recent additions have generally used the Netbeans "Format" right-click action to maintain consistency. Try to maintain reasonable consistency with the existing files. -* Please don't perform wholesale reformatting of a file without discussing with the current maintainers. +## Indentation and White Space + +* Code formatting in the git client plugin varies between files. Recent additions have generally used the Netbeans "Format" right-click action to maintain consistency. Try to maintain reasonable consistency with the existing files +* Please don't reformat a file without discussing with the current maintainers ## Maven POM file layout diff --git a/Jenkinsfile b/Jenkinsfile index b4badc79b8..09a28a145e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,9 +1,6 @@ #!groovy // build both versions, retry test failures -buildPlugin(jenkinsVersions: [null, '2.60.3'], +buildPlugin(jenkinsVersions: [null, '2.121.3'], findbugs: [run:true, archive:true, unstableTotalAll: '0'], failFast: false) - -// No plugin compatibility tests, retry test failures -// buildPlugin(failFast: false) diff --git a/README.md b/README.md index 27e4bb0de8..b2bc2c3fbd 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,36 @@ Utility plugin for Git-related support ====================================== -Extracted from [git-plugin](https://wiki.jenkins-ci.org/display/JENKINS/Git+Plugin) +Extracted from [git-plugin](https://plugins.jenkins.io/git) to make it easier for other plugins to use and contribute new features. Includes JGit as a library so that other Jenkins components can rely on JGit whenever the git client plugin is available. -* see [Jenkins wiki](https://wiki.jenkins-ci.org/display/JENKINS/Git+Client+Plugin) for feature descriptions +* see [Jenkins plugins site](https://plugins.jenkins.io/git-client) for feature descriptions * use [JIRA](https://issues.jenkins-ci.org) to report issues / feature requests Contributing to the Plugin ========================== -Refer to [contributing to the plugin](https://github.com/jenkinsci/git-client-plugin/blob/master/CONTRIBUTING.md) +Refer to [contributing to the plugin](CONTRIBUTING.md) for suggestions to speed the acceptance of your contributions. +Code coverage reporting is available as a maven target and is actively +monitored. Please improve code coverage with the tests you submit. +Code coverage reporting is written to `target/site/jacoco/` by the maven command: + +``` + $ mvn -P enable-jacoco clean install jacoco:report +``` + Building the Plugin =================== +``` $ java -version # Requires Java 1.8 - $ mvn -version # Requires a modern maven version; maven 3.2.5 and 3.5.0 are known to work + $ mvn -version # Requires Apache Maven 3.5.0 or later $ mvn clean install +``` To Do ===== @@ -28,4 +38,4 @@ To Do * Evaluate [pull requests](https://github.com/jenkinsci/git-client-plugin/pulls) * Fix [bugs](https://issues.jenkins-ci.org/secure/IssueNavigator.jspa?mode=hide&reset=true&jqlQuery=project+%3D+JENKINS+AND+status+in+%28Open%2C+"In+Progress"%2C+Reopened%29+AND+component+%3D+git-client-plugin) * Create infrastructure to detect [files opened during a unit test](https://issues.jenkins-ci.org/browse/JENKINS-19994) and left open at exit from test -* Complete more of the JGit implementation +* Complete more JGit implementation diff --git a/pom.xml b/pom.xml index 4b1e583a16..2a460bd677 100644 --- a/pom.xml +++ b/pom.xml @@ -5,13 +5,13 @@ org.jenkins-ci.plugins plugin - 2.37 + 3.26 org.jenkins-ci.plugins git-client - 2.7.1 + ${revision}${changelist} hpi Jenkins Git client plugin @@ -38,37 +38,19 @@ scm:git:git://github.com/jenkinsci/${project.artifactId}-plugin.git scm:git:git@github.com:jenkinsci/${project.artifactId}-plugin.git https://github.com/jenkinsci/${project.artifactId}-plugin - git-client-2.7.1 + ${scmTag} + 3.0.0-beta6 + -SNAPSHOT UTF-8 -Dfile.encoding=${project.build.sourceEncoding} - 2.9 - 1.625.3 - 7 - 4.5.4.201711221230-r - 4.5.0.201609210915-r + 2.60.3 + 8 + 5.1.3.201810200350-r - - - jgit-repository - Eclipse JGit Repository - https://repo.eclipse.org/content/groups/releases/ - - - repo.jenkins-ci.org - https://repo.jenkins-ci.org/public/ - - - - - repo.jenkins-ci.org - https://repo.jenkins-ci.org/public/ - - - org.eclipse.jgit @@ -85,6 +67,11 @@ org.apache.httpcomponents httpclient + + + com.jcraft + jzlib + @@ -95,6 +82,23 @@ org.eclipse.jgit org.eclipse.jgit.http.server ${jgit.version} + + + + com.jcraft + jsch + + + + org.apache.httpcomponents + httpclient + + + + com.jcraft + jzlib + + + com.jcraft + jsch + + + + org.apache.httpcomponents + httpclient + + + + org.apache.httpcomponents + httpcore + + + + com.jcraft + jzlib + + + + + + org.eclipse.jgit + org.eclipse.jgit.lfs + ${jgit.version} + + + + com.jcraft + jsch + + + + org.apache.httpcomponents + httpclient + + + + com.jcraft + jzlib + + org.jenkins-ci.plugins @@ -116,7 +165,7 @@ nl.jqno.equalsverifier equalsverifier - 2.4 + 3.0.2 test @@ -124,9 +173,6 @@ git-server 1.7 test - - - com.googlecode.json-simple @@ -137,7 +183,13 @@ org.jenkins-ci.modules sshd - 1.11 + 2.4 + test + + + org.apache.commons + commons-text + 1.6 test @@ -159,12 +211,12 @@ org.jenkins-ci.plugins apache-httpcomponents-client-4-api - 4.5.3-2.0 + 4.5.5-3.0 org.objenesis objenesis - 2.6 + 3.0.1 test @@ -180,42 +232,56 @@ + + + jgit-repository + Eclipse JGit Repository + https://repo.eclipse.org/content/groups/releases/ + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + maven-compiler-plugin + 3.8.0 maven-surefire-plugin + 2.22.1 maven-javadoc-plugin + 3.0.1 - - maven-compiler-plugin - - 1.7 - 1.7 - - org.apache.maven.plugins maven-javadoc-plugin - org.apache.commons.httpclient.contrib.ssl param,return,throws,link false true false - http://docs.oracle.com/javase/7/docs/api/ - http://download.eclipse.org/jgit/site/${jgit.javadoc.version}/org.eclipse.jgit/apidocs/ + http://docs.oracle.com/javase/8/docs/api/ + http://download.eclipse.org/jgit/site/${jgit.version}/org.eclipse.jgit/apidocs/ + @@ -223,25 +289,6 @@ maven-hpi-plugin true - - org.codehaus.mojo - findbugs-maven-plugin - - ${skipFindbugs} - false - true - true - - - - run-findbugs - verify - - check - - - - maven-surefire-plugin @@ -256,31 +303,15 @@ - - org.apache.maven.plugins - maven-enforcer-plugin - 1.4.2.jenkins-1 - - - - - - org.jenkins-ci.modules:instance-identity - - org.jenkins-ci.modules:ssh-cli-auth - - - - - + org.apache.maven.plugins maven-project-info-reports-plugin - ${maven-project-info-reports-plugin.version} + 2.9 diff --git a/src/main/java/hudson/plugins/git/GitTool.java b/src/main/java/hudson/plugins/git/GitTool.java index 14888df7dd..64eb61a40f 100644 --- a/src/main/java/hudson/plugins/git/GitTool.java +++ b/src/main/java/hudson/plugins/git/GitTool.java @@ -79,9 +79,6 @@ private static GitTool[] getInstallations(DescriptorImpl descriptor) { */ public static GitTool getDefaultInstallation() { Jenkins jenkinsInstance = Jenkins.getInstance(); - if (jenkinsInstance == null) { - return null; - } DescriptorImpl gitTools = jenkinsInstance.getDescriptorByType(GitTool.DescriptorImpl.class); GitTool tool = gitTools.getInstallation(GitTool.DEFAULT); if (tool != null) { @@ -120,9 +117,6 @@ public static void onLoaded() { //Creates default tool installation if needed. Uses "git" or migrates data from previous versions Jenkins jenkinsInstance = Jenkins.getInstance(); - if (jenkinsInstance == null) { - return; - } DescriptorImpl descriptor = (DescriptorImpl) jenkinsInstance.getDescriptor(GitTool.class); GitTool[] installations = getInstallations(descriptor); @@ -192,9 +186,6 @@ public List> getApplicableDesccriptors() { public List> getApplicableDescriptors() { List> r = new ArrayList<>(); Jenkins jenkinsInstance = Jenkins.getInstance(); - if (jenkinsInstance == null) { - return r; - } for (ToolDescriptor td : jenkinsInstance.>getDescriptorList(ToolInstallation.class)) { if (GitTool.class.isAssignableFrom(td.clazz)) { // This checks cast is allowed r.add((ToolDescriptor)td); // This is the unchecked cast diff --git a/src/main/java/hudson/plugins/git/Revision.java b/src/main/java/hudson/plugins/git/Revision.java index 9268468db2..e09f80d861 100644 --- a/src/main/java/hudson/plugins/git/Revision.java +++ b/src/main/java/hudson/plugins/git/Revision.java @@ -1,8 +1,7 @@ package hudson.plugins.git; -import com.google.common.base.Function; -import com.google.common.base.Joiner; -import com.google.common.collect.Iterables; +import static java.util.stream.Collectors.joining; + import hudson.Util; import org.eclipse.jgit.lib.ObjectId; import org.kohsuke.stapler.export.Exported; @@ -22,7 +21,7 @@ public class Revision implements java.io.Serializable, Cloneable { private static final long serialVersionUID = -7203898556389073882L; - ObjectId sha1; + ObjectId sha1; Collection branches; /** @@ -94,42 +93,26 @@ public void setBranches(Collection branches) { } /** - * containsBranchName. + * Returns whether the revision contains the specified branch. * - * @param name a {@link java.lang.String} object. - * @return true if this repository is bare + * @param name the name of the branch + * @return whether the revision contains the branch */ public boolean containsBranchName(String name) { - for (Branch b : branches) { - if (b.getName().equals(name)) { - return true; - } - } - return false; + return branches.stream().anyMatch(branch -> branch.getName().equals(name)); } - /** - * toString. - * - * @return a {@link java.lang.String} object. - */ + @Override public String toString() { final String revisionName = sha1 != null ? sha1.name() : "null"; StringBuilder s = new StringBuilder("Revision " + revisionName + " ("); if (branches != null) { - Joiner.on(", ").appendTo(s, - Iterables.transform(branches, new Function() { - - public String apply(Branch from) { - return Util.fixNull(from.getName()); - } - })); + s.append(branches.stream().map(Branch::getName).map(Util::fixNull).collect(joining(", "))); } s.append(')'); return s.toString(); } - /** {@inheritDoc} */ @Override public Revision clone() { Revision clone; @@ -143,13 +126,11 @@ public Revision clone() { return clone; } - /** {@inheritDoc} */ @Override public int hashCode() { return sha1 != null ? 31 + sha1.hashCode() : 1; } - /** {@inheritDoc} */ @Override public boolean equals(Object obj) { if (!(obj instanceof Revision)) { diff --git a/src/main/java/org/apache/commons/httpclient/contrib/ssl/EasySSLProtocolSocketFactory.java b/src/main/java/org/apache/commons/httpclient/contrib/ssl/EasySSLProtocolSocketFactory.java deleted file mode 100644 index 2bf07daf3c..0000000000 --- a/src/main/java/org/apache/commons/httpclient/contrib/ssl/EasySSLProtocolSocketFactory.java +++ /dev/null @@ -1,222 +0,0 @@ -/* - * $HeadURL$ - * $Revision$ - * $Date$ - * - * ==================================================================== - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * . - * - */ - -package org.apache.commons.httpclient.contrib.ssl; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.io.IOException; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.SocketAddress; -import java.net.UnknownHostException; - -import javax.net.SocketFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; - -import org.apache.commons.httpclient.ConnectTimeoutException; -import org.apache.commons.httpclient.HttpClientError; -import org.apache.commons.httpclient.params.HttpConnectionParams; -import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -/** - *

- * EasySSLProtocolSocketFactory can be used to create SSL {@link java.net.Socket}s - * that accept self-signed certificates. - *

- *

- * This socket factory SHOULD NOT be used for productive systems - * due to security reasons, unless it is a concious decision and - * you are perfectly aware of security implications of accepting - * self-signed certificates - *

- * - *

- * Example of using custom protocol socket factory for a specific host: - *

- *     Protocol easyhttps = new Protocol("https", new EasySSLProtocolSocketFactory(), 443);
- *
- *     URI uri = new URI("https://localhost/", true);
- *     // use relative url only
- *     GetMethod httpget = new GetMethod(uri.getPathQuery());
- *     HostConfiguration hc = new HostConfiguration();
- *     hc.setHost(uri.getHost(), uri.getPort(), easyhttps);
- *     HttpClient client = new HttpClient();
- *     client.executeMethod(hc, httpget);
- *     
- *

- * Example of using custom protocol socket factory per default instead of the standard one: - *

- *     Protocol easyhttps = new Protocol("https", new EasySSLProtocolSocketFactory(), 443);
- *     Protocol.registerProtocol("https", easyhttps);
- *
- *     HttpClient client = new HttpClient();
- *     GetMethod httpget = new GetMethod("https://localhost/");
- *     client.executeMethod(httpget);
- *     
- * - * @author Oleg Kalnichevski - * - *

- * DISCLAIMER: HttpClient developers DO NOT actively support this component. - * The component is provided as a reference material, which may be inappropriate - * for use without additional customization. - *

- */ -public class EasySSLProtocolSocketFactory implements SecureProtocolSocketFactory { - - /** Log object for this class. */ - private static final Log LOG = LogFactory.getLog(EasySSLProtocolSocketFactory.class); - - private SSLContext sslcontext = null; - - /** - * Constructor for EasySSLProtocolSocketFactory. - */ - public EasySSLProtocolSocketFactory() { - super(); - } - - private static SSLContext createEasySSLContext() { - try { - SSLContext context = SSLContext.getInstance("SSL"); - context.init( - null, - new TrustManager[] {new EasyX509TrustManager(null)}, - null); - return context; - } catch (Exception e) { - LOG.error(e.getMessage(), e); - throw new HttpClientError(e.toString()); - } - } - - private SSLContext getSSLContext() { - if (this.sslcontext == null) { - this.sslcontext = createEasySSLContext(); - } - return this.sslcontext; - } - - /** {@inheritDoc} */ - public Socket createSocket( - String host, - int port, - InetAddress clientHost, - int clientPort) - throws IOException, UnknownHostException { - - return getSSLContext().getSocketFactory().createSocket( - host, - port, - clientHost, - clientPort - ); - } - - /** - * {@inheritDoc} - * - * Attempts to get a new socket connection to the given host within the given time limit. - *

- * To circumvent the limitations of older JREs that do not support connect timeout a - * controller thread is executed. The controller thread attempts to create a new socket - * within the given limit of time. If socket constructor does not return until the - * timeout expires, the controller terminates and throws an {@link ConnectTimeoutException} - *

- */ - public Socket createSocket( - final String host, - final int port, - final InetAddress localAddress, - final int localPort, - final HttpConnectionParams params - ) throws IOException, UnknownHostException, ConnectTimeoutException { - if (params == null) { - throw new IllegalArgumentException("Parameters may not be null"); - } - int timeout = params.getConnectionTimeout(); - SocketFactory socketfactory = getSSLContext().getSocketFactory(); - if (timeout == 0) { - return socketfactory.createSocket(host, port, localAddress, localPort); - } else { - Socket socket = socketfactory.createSocket(); - SocketAddress localaddr = new InetSocketAddress(localAddress, localPort); - SocketAddress remoteaddr = new InetSocketAddress(host, port); - socket.bind(localaddr); - socket.connect(remoteaddr, timeout); - return socket; - } - } - - /** {@inheritDoc} */ - public Socket createSocket(String host, int port) - throws IOException, UnknownHostException { - return getSSLContext().getSocketFactory().createSocket( - host, - port - ); - } - - /** {@inheritDoc} */ - public Socket createSocket( - Socket socket, - String host, - int port, - boolean autoClose) - throws IOException, UnknownHostException { - return getSSLContext().getSocketFactory().createSocket( - socket, - host, - port, - autoClose - ); - } - - /** {@inheritDoc} */ - @SuppressFBWarnings(value = "EQ_GETCLASS_AND_CLASS_CONSTANT", - justification = "Implementation provided by Apache, never inherited") - public boolean equals(Object obj) { - return ((obj != null) && obj.getClass().equals(EasySSLProtocolSocketFactory.class)); - } - - /** - * hashCode. - * - * @return a int. - */ - public int hashCode() { - return EasySSLProtocolSocketFactory.class.hashCode(); - } - -} diff --git a/src/main/java/org/apache/commons/httpclient/contrib/ssl/EasyX509TrustManager.java b/src/main/java/org/apache/commons/httpclient/contrib/ssl/EasyX509TrustManager.java deleted file mode 100644 index 63e640a911..0000000000 --- a/src/main/java/org/apache/commons/httpclient/contrib/ssl/EasyX509TrustManager.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * ==================================================================== - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * . - * - */ - -package org.apache.commons.httpclient.contrib.ssl; - -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; - -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -/** - *

- * EasyX509TrustManager unlike default {@link javax.net.ssl.X509TrustManager} accepts - * self-signed certificates. - *

- * This trust manager SHOULD NOT be used for productive systems - * due to security reasons, unless it is a concious decision and - * you are perfectly aware of security implications of accepting - * self-signed certificates - * - *

- * DISCLAIMER: HttpClient developers DO NOT actively support this component. - * The component is provided as a reference material, which may be inappropriate - * for use without additional customization. - * - * @author Adrian Sutton - * @author Oleg Kalnichevski - */ -public class EasyX509TrustManager implements X509TrustManager -{ - private X509TrustManager standardTrustManager = null; - - /** Log object for this class. */ - private static final Log LOG = LogFactory.getLog(EasyX509TrustManager.class); - - /** - * Constructor for EasyX509TrustManager. - * - * @param keystore a {@link java.security.KeyStore} object. - * @throws java.security.NoSuchAlgorithmException if requested algorithm is not available - * @throws java.security.KeyStoreException if KeyStore operations fail - */ - public EasyX509TrustManager(KeyStore keystore) throws NoSuchAlgorithmException, KeyStoreException { - super(); - TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - factory.init(keystore); - TrustManager[] trustmanagers = factory.getTrustManagers(); - if (trustmanagers.length == 0) { - throw new NoSuchAlgorithmException("no trust manager found"); - } - this.standardTrustManager = (X509TrustManager)trustmanagers[0]; - } - - /** {@inheritDoc} */ - public void checkClientTrusted(X509Certificate[] certificates,String authType) throws CertificateException { - standardTrustManager.checkClientTrusted(certificates,authType); - } - - /** {@inheritDoc} */ - public void checkServerTrusted(X509Certificate[] certificates,String authType) throws CertificateException { - if ((certificates != null) && LOG.isDebugEnabled()) { - LOG.debug("Server certificate chain:"); - for (int i = 0; i < certificates.length; i++) { - LOG.debug("X509Certificate[" + i + "]=" + certificates[i]); - } - } - if ((certificates != null) && (certificates.length == 1)) { - certificates[0].checkValidity(); - } else { - standardTrustManager.checkServerTrusted(certificates,authType); - } - } - - /** - * getAcceptedIssuers. - * - * @see javax.net.ssl.X509TrustManager#getAcceptedIssuers() - * @return an array of {@link java.security.cert.X509Certificate} objects. - */ - public X509Certificate[] getAcceptedIssuers() { - return this.standardTrustManager.getAcceptedIssuers(); - } -} diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/AbstractGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/AbstractGitAPIImpl.java index b102d6244d..5f8d307b71 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/AbstractGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/AbstractGitAPIImpl.java @@ -1,11 +1,11 @@ package org.jenkinsci.plugins.gitclient; import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +import hudson.FilePath; import hudson.ProxyConfiguration; import hudson.plugins.git.GitException; import hudson.remoting.Channel; -import jenkins.model.Jenkins.MasterComputer; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Repository; @@ -26,7 +26,7 @@ abstract class AbstractGitAPIImpl implements GitClient, Serializable { /** {@inheritDoc} */ public T withRepository(RepositoryCallback callable) throws IOException, InterruptedException { try (Repository repo = getRepository()) { - return callable.invoke(repo, MasterComputer.localChannel); + return callable.invoke(repo, FilePath.localChannel); } } @@ -90,9 +90,13 @@ public void merge(ObjectId rev) throws GitException, InterruptedException { * When sent to remote, switch to the proxy. * * @return a {@link java.lang.Object} object. + * @throws java.io.ObjectStreamException if current channel is null */ - protected Object writeReplace() { - return remoteProxyFor(Channel.current().export(GitClient.class, this)); + protected Object writeReplace() throws java.io.ObjectStreamException { + Channel currentChannel = Channel.current(); + if (currentChannel == null) + throw new java.io.WriteAbortedException("No current channel", new java.lang.NullPointerException()); + return remoteProxyFor(currentChannel.export(GitClient.class, this)); } /** diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/ChangelogCommand.java b/src/main/java/org/jenkinsci/plugins/gitclient/ChangelogCommand.java index 96c2a0ee67..3b12b938dc 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/ChangelogCommand.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/ChangelogCommand.java @@ -9,7 +9,7 @@ * Command builder for generating changelog in the format {@code GitSCM} expects. * *

- * The output format is that of git-whatchanged, which looks something like this: + * The output format is that of git-whatchanged, which looks something like this: * *

  * commit dadaf808d99c4c23c53476b0c48e25a181016300
@@ -121,4 +121,11 @@ public interface ChangelogCommand extends GitCommand {
      * ChangelogCommand instance or files will be left open.
      */
     void abort();
+    
+    /**
+     * Include merge commits in the changelog
+     * @param flag true if merge commits should be listed
+     * @return a {@link org.jenkinsci.plugins.gitclient.ChangelogCommand} object.
+     */
+    ChangelogCommand listMerges(boolean flag);
 }
diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java
index 68e8e5eb1c..e1325e211e 100644
--- a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java
+++ b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java
@@ -25,8 +25,10 @@
 import hudson.plugins.git.Revision;
 import hudson.util.ArgumentListBuilder;
 import hudson.util.Secret;
+import hudson.Proc;
 
 import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang.StringUtils;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
@@ -45,19 +47,28 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.nio.file.attribute.AclEntry;
+import java.nio.file.attribute.AclEntryPermission;
+import java.nio.file.attribute.AclEntryType;
+import java.nio.file.attribute.AclFileAttributeView;
 import java.nio.file.attribute.FileAttribute;
 import java.nio.file.attribute.PosixFilePermission;
 import java.nio.file.attribute.PosixFilePermissions;
+import java.nio.file.attribute.UserPrincipal;
+import java.nio.file.attribute.UserPrincipalLookupService;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 import java.util.regex.Pattern;
 import java.util.regex.Matcher;
@@ -124,6 +135,29 @@ public class CliGitAPIImpl extends LegacyCompatibleGitAPIImpl {
      * ssh configurations).
      */
     private static final boolean CALL_SETSID;
+
+    /**
+     * Needed file permission for OpenSSH client that is made by Windows,
+     * this will remove unwanted users and inherited permissions
+     * which is required when the git client is using the SSH to clone
+     *
+     * The ssh client that the git client ships ignores file permission on Windows
+     * Which the PowerShell team at Microsoft decided to fix in their port of OpenSSH
+     */
+    static final EnumSet ACL_ENTRY_PERMISSIONS = EnumSet.of(
+        AclEntryPermission.READ_DATA,
+        AclEntryPermission.WRITE_DATA,
+        AclEntryPermission.APPEND_DATA,
+        AclEntryPermission.READ_NAMED_ATTRS,
+        AclEntryPermission.WRITE_NAMED_ATTRS,
+        AclEntryPermission.EXECUTE,
+        AclEntryPermission.READ_ATTRIBUTES,
+        AclEntryPermission.WRITE_ATTRIBUTES,
+        AclEntryPermission.DELETE,
+        AclEntryPermission.READ_ACL,
+        AclEntryPermission.SYNCHRONIZE
+    );
+
     static {
         acceptSelfSignedCertificates = Boolean.getBoolean(GitClient.class.getName() + ".untrustedSSL");
         CALL_SETSID = setsidExists() && USE_SETSID;
@@ -141,6 +175,7 @@ public class CliGitAPIImpl extends LegacyCompatibleGitAPIImpl {
     private Map credentials = new HashMap<>();
     private StandardCredentials defaultCredentials;
     private StandardCredentials lfsCredentials;
+    private final String encoding;
 
     /* git config --get-regex applies the regex to match keys, and returns all matches (including substring matches).
      * Thus, a config call:
@@ -243,14 +278,19 @@ private void getGitVersion() {
      * @param listener a {@link hudson.model.TaskListener} object.
      * @param environment a {@link hudson.EnvVars} object.
      */
-    protected CliGitAPIImpl(String gitExe, File workspace,
-                         TaskListener listener, EnvVars environment) {
+    protected CliGitAPIImpl(String gitExe, File workspace, TaskListener listener, EnvVars environment) {
         super(workspace);
         this.listener = listener;
         this.gitExe = gitExe;
         this.environment = environment;
+        
+        if( isZos() && System.getProperty("ibm.system.encoding") != null ) { 
+            this.encoding = Charset.forName(System.getProperty("ibm.system.encoding")).toString();
+        } else {
+            this.encoding = Charset.defaultCharset().toString();
+        }
 
-        launcher = new LocalLauncher(IGitAPI.verbose?listener:TaskListener.NULL);
+        launcher = new LocalLauncher(IGitAPI.verbose ? listener : TaskListener.NULL);
     }
 
     /** {@inheritDoc} */
@@ -329,49 +369,57 @@ public List getSubmodules( String treeIsh ) throws GitException, Int
      */
     public FetchCommand fetch_() {
         return new FetchCommand() {
-            public URIish url;
-            public List refspecs;
-            public boolean prune;
-            public boolean shallow;
-            public Integer timeout;
-            public boolean tags = true;
-            public Integer depth = 1;
+            private URIish url;
+            private List refspecs;
+            private boolean prune;
+            private boolean shallow;
+            private Integer timeout;
+            private boolean tags = true;
+            private Integer depth = 1;
 
+            @Override
             public FetchCommand from(URIish remote, List refspecs) {
                 this.url = remote;
                 this.refspecs = refspecs;
                 return this;
             }
 
+            @Override
             public FetchCommand tags(boolean tags) {
                 this.tags = tags;
                 return this;
             }
 
+            @Override
             public FetchCommand prune() {
                 return prune(true);
             }
 
+            @Override
             public FetchCommand prune(boolean prune) {
                 this.prune = prune;
                 return this;
             }
 
+            @Override
             public FetchCommand shallow(boolean shallow) {
                 this.shallow = shallow;
                 return this;
             }
 
+            @Override
             public FetchCommand timeout(Integer timeout) {
             	this.timeout = timeout;
             	return this;
             }
 
+            @Override
             public FetchCommand depth(Integer depth) {
                 this.depth = depth;
                 return this;
             }
 
+            @Override
             public void execute() throws GitException, InterruptedException {
                 listener.getLogger().println(
                         "Fetching upstream changes from " + url);
@@ -394,7 +442,7 @@ public void execute() throws GitException, InterruptedException {
                 if (prune) args.add("--prune");
 
                 if (shallow) {
-                    if (depth == null){
+                    if (depth == null) {
                         depth = 1;
                     }
                     args.add("--depth=" + depth);
@@ -402,7 +450,22 @@ public void execute() throws GitException, InterruptedException {
 
                 warnIfWindowsTemporaryDirNameHasSpaces();
 
-                launchCommandWithCredentials(args, workspace, cred, url, timeout);
+                /* If url looks like a remote name reference, convert to remote URL for authentication */
+                /* See JENKINS-50573 for more details */
+                /* "git remote add" rejects remote names with ':' (and it is a common character in remote URLs) */
+                /* "git remote add" allows remote names with '@' but internal git parsing problems seem likely (and it is a common character in remote URLs) */
+                /* "git remote add" allows remote names with '/' but git client plugin parsing problems will occur (and it is a common character in remote URLs) */
+                /* "git remote add" allows remote names with '\' but git client plugin parsing problems will occur */
+                URIish remoteUrl = url;
+                if (!url.isRemote() && !StringUtils.containsAny(url.toString(), ":@/\\")) {
+                    try {
+                        remoteUrl = new URIish(getRemoteUrl(url.toString()));
+                    } catch (URISyntaxException e) {
+                        listener.getLogger().println("Unexpected remote name or URL: '" + url + "'");
+                    }
+                }
+
+                launchCommandWithCredentials(args, workspace, cred, remoteUrl, timeout);
             }
         };
     }
@@ -470,25 +533,28 @@ public void reset(boolean hard) throws GitException, InterruptedException {
      */
     public CloneCommand clone_() {
         return new CloneCommand() {
-            String url;
-            String origin = "origin";
-            String reference;
-            boolean shallow,shared;
-            Integer timeout;
-            boolean tags = true;
-            List refspecs;
-            Integer depth = 1;
+            private String url;
+            private String origin = "origin";
+            private String reference;
+            private boolean shallow,shared;
+            private Integer timeout;
+            private boolean tags = true;
+            private List refspecs;
+            private Integer depth = 1;
 
+            @Override
             public CloneCommand url(String url) {
                 this.url = url;
                 return this;
             }
 
+            @Override
             public CloneCommand repositoryName(String name) {
                 this.origin = name;
                 return this;
             }
 
+            @Override
             public CloneCommand shared() {
                 return shared(true);
             }
@@ -499,6 +565,7 @@ public CloneCommand shared(boolean shared) {
                 return this;
             }
 
+            @Override
             public CloneCommand shallow() {
                 return shallow(true);
             }
@@ -509,36 +576,43 @@ public CloneCommand shallow(boolean shallow) {
                 return this;
             }
 
+            @Override
             public CloneCommand noCheckout() {
                 //this.noCheckout = true; Since the "clone" command has been replaced with init + fetch, the --no-checkout option is always satisfied
                 return this;
             }
 
+            @Override
             public CloneCommand tags(boolean tags) {
                 this.tags = tags;
                 return this;
             }
 
+            @Override
             public CloneCommand reference(String reference) {
                 this.reference = reference;
                 return this;
             }
 
+            @Override
             public CloneCommand timeout(Integer timeout) {
             	this.timeout = timeout;
             	return this;
             }
 
+            @Override
             public CloneCommand depth(Integer depth) {
                 this.depth = depth;
                 return this;
             }
 
+            @Override
             public CloneCommand refspecs(List refspecs) {
                 this.refspecs = new ArrayList<>(refspecs);
                 return this;
             }
 
+            @Override
             public void execute() throws GitException, InterruptedException {
 
                 URIish urIish = null;
@@ -628,43 +702,50 @@ else if (!referencePath.isDirectory())
      */
     public MergeCommand merge() {
         return new MergeCommand() {
-            public ObjectId rev;
-            public String comment;
-            public String strategy;
-            public String fastForwardMode;
-            public boolean squash;
-            public boolean commit = true;
+            private ObjectId rev;
+            private String comment;
+            private String strategy;
+            private String fastForwardMode;
+            private boolean squash;
+            private boolean commit = true;
 
+            @Override
             public MergeCommand setRevisionToMerge(ObjectId rev) {
                 this.rev = rev;
                 return this;
             }
 
+            @Override
             public MergeCommand setStrategy(MergeCommand.Strategy strategy) {
                 this.strategy = strategy.toString();
                 return this;
             }
 
+            @Override
             public MergeCommand setGitPluginFastForwardMode(MergeCommand.GitPluginFastForwardMode fastForwardMode) {
                 this.fastForwardMode = fastForwardMode.toString();
                 return this;
             }
 
+            @Override
             public MergeCommand setSquash(boolean squash) {
                 this.squash = squash;
                 return this;
             }
 
+            @Override
             public MergeCommand setMessage(String comment) {
                 this.comment = comment;
                 return this;
             }
 
+            @Override
             public MergeCommand setCommit(boolean commit) {
                 this.commit = commit;
                 return this;
             }
 
+            @Override
             public void execute() throws GitException, InterruptedException {
                 ArgumentListBuilder args = new ArgumentListBuilder();
                 args.add("merge");
@@ -708,11 +789,13 @@ public RebaseCommand rebase() {
         return new RebaseCommand() {
             private String upstream;
 
+            @Override
             public RebaseCommand setUpstream(String upstream) {
                 this.upstream = upstream;
                 return this;
             }
 
+            @Override
             public void execute() throws GitException, InterruptedException {
                 try {
                     ArgumentListBuilder args = new ArgumentListBuilder();
@@ -735,19 +818,22 @@ public void execute() throws GitException, InterruptedException {
     public InitCommand init_() {
         return new InitCommand() {
 
-            public String workspace;
-            public boolean bare;
+            private String workspace;
+            private boolean bare;
 
+            @Override
             public InitCommand workspace(String workspace) {
                 this.workspace = workspace;
                 return this;
             }
 
+            @Override
             public InitCommand bare(boolean bare) {
                 this.bare = bare;
                 return this;
             }
 
+            @Override
             public void execute() throws GitException, InterruptedException {
                 /* Match JGit - create directory if it does not exist */
                 /* Multi-branch pipeline assumes init() creates directory */
@@ -779,12 +865,27 @@ public void execute() throws GitException, InterruptedException {
      * Remove untracked files and directories, including files listed
      * in the ignore rules.
      *
+     * @param cleanSubmodule flag to add extra -f
      * @throws hudson.plugins.git.GitException if underlying git operation fails.
      * @throws java.lang.InterruptedException if interrupted.
      */
-    public void clean() throws GitException, InterruptedException {
+    public void clean(boolean cleanSubmodule) throws GitException, InterruptedException {
         reset(true);
-        launchCommand("clean", "-fdx");
+	String cmd = "-fdx";
+	if (cleanSubmodule) cmd = "-ffdx";
+
+	launchCommand("clean", cmd);
+    }
+
+    /**
+     * Remove untracked files and directories, including files listed
+     * in the ignore rules.
+     *
+     * @throws hudson.plugins.git.GitException if underlying git operation fails.
+     * @throws java.lang.InterruptedException if interrupted.
+     */
+    public void clean() throws GitException, InterruptedException {
+        this.clean(false);
     }
 
     /** {@inheritDoc} */
@@ -907,11 +1008,12 @@ public ChangelogCommand changelog() {
         return new ChangelogCommand() {
 
             /** Equivalent to the git-log raw format but using ISO 8601 date format - also prevent to depend on git CLI future changes */
-            public static final String RAW = "commit %H%ntree %T%nparent %P%nauthor %aN <%aE> %ai%ncommitter %cN <%cE> %ci%n%n%w(76,4,4)%s%n%n%b";
-            final List revs = new ArrayList<>();
+            public static final String RAW = "commit %H%ntree %T%nparent %P%nauthor %aN <%aE> %ai%ncommitter %cN <%cE> %ci%n%n%w(0,4,4)%B";
+            private final List revs = new ArrayList<>();
 
-            Integer n = null;
-            Writer out = null;
+            private Integer n = null;
+            private Writer out = null;
+            private boolean listMerges = false;
 
             @Override
             public ChangelogCommand excludes(String rev) {
@@ -958,6 +1060,8 @@ public void execute() throws GitException, InterruptedException {
                 args.add("--format="+RAW);
                 if (n!=null)
                     args.add("-n").add(n);
+                if (listMerges)
+                    args.add("-m");
                 for (String rev : this.revs)
                     args.add(rev);
 
@@ -973,6 +1077,11 @@ public void execute() throws GitException, InterruptedException {
                     throw new GitException("Error: " + args + " in " + workspace, e);
                 }
             }
+
+            public ChangelogCommand listMerges(boolean flag) {
+                listMerges = flag;
+                return this;
+            }
         };
     }
 
@@ -1034,47 +1143,75 @@ public void submoduleSync() throws GitException, InterruptedException {
      */
     public SubmoduleUpdateCommand submoduleUpdate() {
         return new SubmoduleUpdateCommand() {
-            boolean recursive                      = false;
-            boolean remoteTracking                 = false;
-            boolean parentCredentials              = false;
-            String  ref                            = null;
-            Map submodBranch   = new HashMap<>();
-            public Integer timeout;
+            private boolean recursive                      = false;
+            private boolean remoteTracking                 = false;
+            private boolean parentCredentials              = false;
+            private boolean shallow                        = false;
+            private String  ref                            = null;
+            private Map submodBranch   = new HashMap<>();
+            private Integer timeout;
+            private Integer depth = 1;
+            private Integer threads = 1;
 
+            @Override
             public SubmoduleUpdateCommand recursive(boolean recursive) {
                 this.recursive = recursive;
                 return this;
             }
 
+            @Override
             public SubmoduleUpdateCommand remoteTracking(boolean remoteTracking) {
                 this.remoteTracking = remoteTracking;
                 return this;
             }
 
+            @Override
             public SubmoduleUpdateCommand parentCredentials(boolean parentCredentials) {
                 this.parentCredentials = parentCredentials;
                 return this;
             }
 
+            @Override
             public SubmoduleUpdateCommand ref(String ref) {
                 this.ref = ref;
                 return this;
             }
 
+            @Override
             public SubmoduleUpdateCommand useBranch(String submodule, String branchname) {
                 this.submodBranch.put(submodule, branchname);
                 return this;
             }
 
+            @Override
             public SubmoduleUpdateCommand timeout(Integer timeout) {
                 this.timeout = timeout;
                 return this;
             }
 
+            @Override
+            public SubmoduleUpdateCommand shallow(boolean shallow) {
+                this.shallow = shallow;
+                return this;
+            }
+
+            @Override
+            public SubmoduleUpdateCommand depth(Integer depth) {
+                this.depth = depth;
+                return this;
+            }
+
+            @Override
+            public SubmoduleUpdateCommand threads(Integer threads) {
+                this.threads = threads;
+                return this;
+            }
+
             /**
              * @throws GitException if executing the Git command fails
              * @throws InterruptedException if called methods throw same exception
              */
+            @Override
             public void execute() throws GitException, InterruptedException {
                 // Initialize the submodules to ensure that the git config
                 // contains the URLs from .gitmodules.
@@ -1101,7 +1238,16 @@ else if (!referencePath.isDirectory())
                     else
                         args.add("--reference", ref);
                 }
-
+                if (shallow) {
+                    if (depth == null) {
+                        depth = 1;
+                    }
+                    if (isAtLeastVersion(1, 8, 4, 0)) {
+                        args.add("--depth=" + depth);
+                    } else {
+                        listener.getLogger().println("[WARNING] Git client older than 1.8.4 doesn't support shallow submodule updates. This flag is ignored.");
+                    }
+                }
 
                 // We need to call submodule update for each configured
                 // submodule. Note that we can't reliably depend on the
@@ -1125,6 +1271,14 @@ else if (!referencePath.isDirectory())
                 // path.
                 Pattern pattern = Pattern.compile(SUBMODULE_REMOTE_PATTERN_STRING, Pattern.MULTILINE);
                 Matcher matcher = pattern.matcher(cfgOutput);
+
+                ExecutorService executorService;
+                if (threads > 1) {
+                    executorService = Executors.newFixedThreadPool(threads);
+                } else {
+                    executorService = Executors.newSingleThreadExecutor();
+                }
+
                 while (matcher.find()) {
                     ArgumentListBuilder perModuleArgs = args.clone();
                     String sModuleName = matcher.group(1);
@@ -1158,8 +1312,19 @@ else if (!referencePath.isDirectory())
                     String sModulePath = getSubmodulePath(sModuleName);
 
                     perModuleArgs.add(sModulePath);
-                    launchCommandWithCredentials(perModuleArgs, workspace, cred, urIish, timeout);
+                    StandardCredentials finalCred = cred;
+                    URIish finalUrIish = urIish;
+                    executorService.submit(() -> {
+                        try {
+                            launchCommandWithCredentials(perModuleArgs, workspace, finalCred, finalUrIish, timeout);
+                        } catch (InterruptedException e) {
+                            throw new GitException("Interrupted while updating submodule for " + sModuleName);
+                        }
+                    });
                 }
+
+                executorService.shutdown();
+                executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
             }
         };
     }
@@ -1526,12 +1691,19 @@ private File createTempFileInSystemDir(String prefix, String suffix) throws IOEx
      *
      * Package protected for testing.  Not to be used outside this class
      *
-     * @param prefix file name prefix for the generated temporary file
+     * @param prefix file name prefix for the generated temporary file (will be preceeded by "jenkins-gitclient-")
      * @param suffix file name suffix for the generated temporary file
      * @return temporary file
      * @throws IOException on error
      */
     File createTempFile(String prefix, String suffix) throws IOException {
+        String common_prefix = "jenkins-gitclient-";
+        if (prefix == null) {
+            prefix = common_prefix;
+        } else {
+            prefix = common_prefix + prefix;
+        }
+
         if (workspace == null) {
             return createTempFileInSystemDir(prefix, suffix);
         }
@@ -1555,6 +1727,9 @@ File createTempFile(String prefix, String suffix) throws IOException {
                 return createTempFileInSystemDir(prefix, suffix);
             }
             return Files.createTempFile(tmpPath, prefix, suffix).toFile();
+        } else if (workspaceTmp.getAbsolutePath().contains("%")) {
+            /* Avoid Linux expansion of % in ssh arguments */
+            return createTempFileInSystemDir(prefix, suffix);
         }
         // Unix specific
         if (workspaceTmp.getAbsolutePath().contains("`")) {
@@ -1631,14 +1806,16 @@ private String launchCommandWithCredentials(ArgumentListBuilder args, File workD
 
         File key = null;
         File ssh = null;
-        File pass = null;
         File askpass = null;
+        File usernameFile = null;
+        File passwordFile = null;
+        File passphrase = null;
         EnvVars env = environment;
         if (!PROMPT_FOR_AUTHENTICATION && isAtLeastVersion(2, 3, 0, 0)) {
             env = new EnvVars(env);
-            env.put("GIT_TERMINAL_PROMPT", "0"); // Don't prompt for auth from command line git
+            env.put("GIT_TERMINAL_PROMPT", "false"); // Don't prompt for auth from command line git
             if (isWindows()) {
-                env.put("GCM_INTERACTIVE", "never"); // Don't prompt for auth from git credentials manager for windows
+                env.put("GCM_INTERACTIVE", "false"); // Don't prompt for auth from git credentials manager for windows
             }
         }
         try {
@@ -1647,18 +1824,25 @@ private String launchCommandWithCredentials(ArgumentListBuilder args, File workD
                 listener.getLogger().println("using GIT_SSH to set credentials " + sshUser.getDescription());
 
                 key = createSshKeyFile(sshUser);
+                // Prefer url username if set, OpenSSH 7.7 argument precedence change
+                // See JENKINS-50573 for details
+                String userName = url.getUser();
+                if (userName == null) {
+                    userName = sshUser.getUsername();
+                }
+                passphrase = createPassphraseFile(sshUser);
                 if (launcher.isUnix()) {
-                    ssh =  createUnixGitSSH(key, sshUser.getUsername());
-                    pass =  createUnixSshAskpass(sshUser);
+                    ssh =  createUnixGitSSH(key, userName);
+                    askpass =  createUnixSshAskpass(sshUser, passphrase);
                 } else {
-                    ssh =  createWindowsGitSSH(key, sshUser.getUsername());
-                    pass =  createWindowsSshAskpass(sshUser);
+                    ssh = createWindowsGitSSH(key, userName);
+                    askpass =  createWindowsSshAskpass(sshUser, passphrase);
                 }
 
                 env = new EnvVars(env);
                 env.put("GIT_SSH", ssh.getAbsolutePath());
                 env.put("GIT_SSH_VARIANT", "ssh");
-                env.put("SSH_ASKPASS", pass.getAbsolutePath());
+                env.put("SSH_ASKPASS", askpass.getAbsolutePath());
 
                 // supply a dummy value for DISPLAY if not already present
                 // or else ssh will not invoke SSH_ASKPASS
@@ -1670,15 +1854,16 @@ private String launchCommandWithCredentials(ArgumentListBuilder args, File workD
                 StandardUsernamePasswordCredentials userPass = (StandardUsernamePasswordCredentials) credentials;
                 listener.getLogger().println("using GIT_ASKPASS to set credentials " + userPass.getDescription());
 
+                usernameFile = createUsernameFile(userPass);
+                passwordFile = createPasswordFile(userPass);
                 if (launcher.isUnix()) {
-                    askpass = createUnixStandardAskpass(userPass);
+                    askpass = createUnixStandardAskpass(userPass, usernameFile, passwordFile);
                 } else {
-                    askpass = createWindowsStandardAskpass(userPass);
+                    askpass = createWindowsStandardAskpass(userPass, usernameFile, passwordFile);
                 }
 
                 env = new EnvVars(env);
                 env.put("GIT_ASKPASS", askpass.getAbsolutePath());
-                // SSH binary does not recognize GIT_ASKPASS, so set SSH_ASKPASS also, in the case we have an ssh:// URL
                 env.put("SSH_ASKPASS", askpass.getAbsolutePath());
             }
 
@@ -1716,101 +1901,169 @@ private String launchCommandWithCredentials(ArgumentListBuilder args, File workD
         } catch (IOException e) {
             throw new GitException("Failed to setup credentials", e);
         } finally {
-            deleteTempFile(pass);
             deleteTempFile(key);
             deleteTempFile(ssh);
             deleteTempFile(askpass);
+            deleteTempFile(passphrase);
+            deleteTempFile(usernameFile);
+            deleteTempFile(passwordFile);
         }
     }
 
     private File createSshKeyFile(SSHUserPrivateKey sshUser) throws IOException, InterruptedException {
         File key = createTempFile("ssh", ".key");
-        try (PrintWriter w = new PrintWriter(key, Charset.defaultCharset().toString())) {
+        try (PrintWriter w = new PrintWriter(key, encoding)) {
             List privateKeys = sshUser.getPrivateKeys();
             for (String s : privateKeys) {
                 w.println(s);
             }
         }
-        new FilePath(key).chmod(0400);
+        if (launcher.isUnix()) {
+            new FilePath(key).chmod(0400);
+        } else {
+            fixSshKeyOnWindows(key);
+        }
+
         return key;
     }
 
     /* package protected for testability */
-    String escapeWindowsCharsForUnquotedString(String str) {
-        // Quote special characters for Windows Batch Files
-        // See: http://stackoverflow.com/questions/562038/escaping-double-quotes-in-batch-script
-        // See: http://ss64.com/nt/syntax-esc.html
-        String quoted = str.replace("%", "%%")
-                        .replace("^", "^^")
-                        .replace(" ", "^ ")
-                        .replace("\t", "^\t")
-                        .replace("\\", "^\\")
-                        .replace("&", "^&")
-                        .replace("|", "^|")
-                        .replace("\"", "^\"")
-                        .replace(">", "^>")
-                        .replace("<", "^<");
-        return quoted;
-    }
-
-    private String quoteUnixCredentials(String str) {
-        // Assumes string will be used inside of single quotes, as it will
-        // only replace "'" substrings.
-        return str.replace("'", "'\\''");
-    }
-
-    private File createWindowsSshAskpass(SSHUserPrivateKey sshUser) throws IOException {
-        File ssh = createTempFile("pass", ".bat");
-        try (PrintWriter w = new PrintWriter(ssh, Charset.defaultCharset().toString())) {
+    void fixSshKeyOnWindows(File key) throws GitException {
+        if (launcher.isUnix()) return;
+
+        Path file = Paths.get(key.toURI());
+
+        AclFileAttributeView fileAttributeView = Files.getFileAttributeView(file, AclFileAttributeView.class);
+        if (fileAttributeView == null) return;
+
+        String username = getWindowsUserName(fileAttributeView);
+        if (StringUtils.isBlank(username)) return;
+
+        try {
+            UserPrincipalLookupService userPrincipalLookupService = file.getFileSystem().getUserPrincipalLookupService();
+            UserPrincipal userPrincipal = userPrincipalLookupService.lookupPrincipalByName(username);
+            AclEntry aclEntry = AclEntry.newBuilder()
+                .setType(AclEntryType.ALLOW)
+                .setPrincipal(userPrincipal)
+                .setPermissions(ACL_ENTRY_PERMISSIONS)
+                .build();
+            fileAttributeView.setAcl(Collections.singletonList(aclEntry));
+        } catch (IOException | UnsupportedOperationException e) {
+            throw new GitException("Error updating file permission for \"" + key.getAbsolutePath() + "\"");
+        }
+    }
+
+    /* package protected for testability */
+    String getWindowsUserName(AclFileAttributeView aclFileAttributeView) {
+        if (launcher.isUnix()) return "";
+
+        try {
+            return aclFileAttributeView.getOwner().getName();
+        } catch (IOException ignored) {
+            String username = System.getenv("USERNAME");
+            if (StringUtils.isBlank(username)) return "";
+
+            String domain = System.getenv("USERDOMAIN");
+            if (StringUtils.isNotBlank(domain) && !username.endsWith("$")) {
+                username = domain + "\\" + username;
+            } else if (username.endsWith("$")) {
+                username = "BUILTIN\\Administrators";
+            }
+
+            return username;
+        }
+    }
+
+    /* Escape all double quotes in filename, then surround filename in double quotes.
+     * Only useful to prepare filename for reference from a DOS batch file.
+     */
+    private String windowsArgEncodeFileName(String filename) {
+        if (filename.contains("\"")) {
+            filename = filename.replaceAll("\"", "^\"");
+        }
+        return "\"" + filename + "\"";
+    }
+
+    private File createWindowsSshAskpass(SSHUserPrivateKey sshUser, @NonNull File passphrase) throws IOException {
+        File ssh = File.createTempFile("pass", ".bat");
+        try (PrintWriter w = new PrintWriter(ssh, encoding)) {
             // avoid echoing command as part of the password
             w.println("@echo off");
-            // no surrounding double quotes on windows echo -- they are echoed too
-            w.println("echo " + escapeWindowsCharsForUnquotedString(Secret.toString(sshUser.getPassphrase())));
+            w.println("type " + windowsArgEncodeFileName(passphrase.getAbsolutePath()));
             w.flush();
         }
         ssh.setExecutable(true, true);
         return ssh;
     }
 
-    private File createUnixSshAskpass(SSHUserPrivateKey sshUser) throws IOException {
+    /* Escape all single quotes in filename, then surround filename in single quotes.
+     * Only useful to prepare filename for reference from a shell script.
+     */
+    private String unixArgEncodeFileName(String filename) {
+        if (filename.contains("'")) {
+            filename = filename.replaceAll("'", "\\'");
+        }
+        return "'" + filename + "'";
+    }
+
+    private File createUnixSshAskpass(SSHUserPrivateKey sshUser, @NonNull File passphrase) throws IOException {
         File ssh = createTempFile("pass", ".sh");
-        try (PrintWriter w = new PrintWriter(ssh, Charset.defaultCharset().toString())) {
+        try (PrintWriter w = new PrintWriter(ssh, encoding)) {
             w.println("#!/bin/sh");
-            w.println("echo '" + quoteUnixCredentials(Secret.toString(sshUser.getPassphrase())) + "'");
+            w.println("cat " + unixArgEncodeFileName(passphrase.getAbsolutePath()));
         }
         ssh.setExecutable(true, true);
         return ssh;
     }
 
-    /* Package protected for testability */
-    File createWindowsBatFile(String userName, String password) throws IOException {
+    private File createWindowsStandardAskpass(StandardUsernamePasswordCredentials creds, File usernameFile, File passwordFile) throws IOException {
         File askpass = createTempFile("pass", ".bat");
-        try (PrintWriter w = new PrintWriter(askpass, Charset.defaultCharset().toString())) {
+        try (PrintWriter w = new PrintWriter(askpass, encoding)) {
             w.println("@set arg=%~1");
-            w.println("@if (%arg:~0,8%)==(Username) echo " + escapeWindowsCharsForUnquotedString(userName));
-            w.println("@if (%arg:~0,8%)==(Password) echo " + escapeWindowsCharsForUnquotedString(password));
+            w.println("@if (%arg:~0,8%)==(Username) type " + windowsArgEncodeFileName(usernameFile.getAbsolutePath()));
+            w.println("@if (%arg:~0,8%)==(Password) type " + windowsArgEncodeFileName(passwordFile.getAbsolutePath()));
         }
         askpass.setExecutable(true, true);
         return askpass;
     }
 
-    private File createWindowsStandardAskpass(StandardUsernamePasswordCredentials creds) throws IOException {
-        return createWindowsBatFile(creds.getUsername(), Secret.toString(creds.getPassword()));
-    }
-
-    private File createUnixStandardAskpass(StandardUsernamePasswordCredentials creds) throws IOException {
+    private File createUnixStandardAskpass(StandardUsernamePasswordCredentials creds, File usernameFile, File passwordFile) throws IOException {
         File askpass = createTempFile("pass", ".sh");
-        try (PrintWriter w = new PrintWriter(askpass, Charset.defaultCharset().toString())) {
+        try (PrintWriter w = new PrintWriter(askpass, encoding)) {
             w.println("#!/bin/sh");
             w.println("case \"$1\" in");
-            w.println("Username*) echo '" + quoteUnixCredentials(creds.getUsername()) + "' ;;");
-            w.println("Password*) echo '" + quoteUnixCredentials(Secret.toString(creds.getPassword())) + "' ;;");
+            w.println("Username*) cat " + unixArgEncodeFileName(usernameFile.getAbsolutePath()) + " ;;");
+            w.println("Password*) cat " + unixArgEncodeFileName(passwordFile.getAbsolutePath()) + " ;;");
             w.println("esac");
         }
         askpass.setExecutable(true, true);
         return askpass;
     }
 
+    private File createPassphraseFile(SSHUserPrivateKey sshUser) throws IOException {
+        File passphraseFile = createTempFile("phrase", ".txt");
+        try (PrintWriter w = new PrintWriter(passphraseFile, "UTF-8")) {
+            w.println(Secret.toString(sshUser.getPassphrase()));
+        }
+        return passphraseFile;
+    }
+
+    private File createUsernameFile(StandardUsernamePasswordCredentials userPass) throws IOException {
+        File usernameFile = createTempFile("username", ".txt");
+        try (PrintWriter w = new PrintWriter(usernameFile, "UTF-8")) {
+            w.println(userPass.getUsername());
+        }
+        return usernameFile;
+    }
+
+    private File createPasswordFile(StandardUsernamePasswordCredentials userPass) throws IOException {
+        File passwordFile = createTempFile("password", ".txt");
+        try (PrintWriter w = new PrintWriter(passwordFile, "UTF-8")) {
+            w.println(Secret.toString(userPass.getPassword()));
+        }
+        return passwordFile;
+    }
+
     private String getPathToExe(String userGitExe) {
         userGitExe = userGitExe.toLowerCase();
 
@@ -1933,7 +2186,7 @@ private File createWindowsGitSSH(File key, String user) throws IOException {
 
         File sshexe = getSSHExecutable();
 
-        try (PrintWriter w = new PrintWriter(ssh, Charset.defaultCharset().toString())) {
+        try (PrintWriter w = new PrintWriter(ssh, encoding)) {
             w.println("@echo off");
             w.println("\"" + sshexe.getAbsolutePath() + "\" -i \"" + key.getAbsolutePath() +"\" -l \"" + user + "\" -o StrictHostKeyChecking=no %* ");
         }
@@ -1943,7 +2196,9 @@ private File createWindowsGitSSH(File key, String user) throws IOException {
 
     private File createUnixGitSSH(File key, String user) throws IOException {
         File ssh = createTempFile("ssh", ".sh");
-        try (PrintWriter w = new PrintWriter(ssh, Charset.defaultCharset().toString())) {
+        File ssh_copy = new File(ssh.toString() + "-copy");
+        boolean isCopied = false;
+        try (PrintWriter w = new PrintWriter(ssh, encoding)) {
             w.println("#!/bin/sh");
             // ${SSH_ASKPASS} might be ignored if ${DISPLAY} is not set
             w.println("if [ -z \"${DISPLAY}\" ]; then");
@@ -1953,7 +2208,31 @@ private File createUnixGitSSH(File key, String user) throws IOException {
             w.println("ssh -i \"" + key.getAbsolutePath() + "\" -l \"" + user + "\" -o StrictHostKeyChecking=no \"$@\"");
         }
         ssh.setExecutable(true, true);
-        return ssh;
+        //JENKINS-48258 git client plugin occasionally fails with "text file busy" error
+        //The following creates a copy of the generated file and deletes the original
+        //In case of a failure return the original and delete the copy
+        String fromLocation = ssh.toString();
+        String toLocation = ssh_copy.toString();
+        //Copying ssh file
+        try {
+            new ProcessBuilder("cp", fromLocation, toLocation).start().waitFor();
+            isCopied = true;
+            ssh_copy.setExecutable(true,true);
+            //Deleting original file
+            deleteTempFile(ssh);
+        }
+        catch(InterruptedException ie)
+        {
+            //Delete the copied file in case of failure
+            if(isCopied)
+            {
+                deleteTempFile(ssh_copy);
+            }
+            //Previous operation failed. Return original file
+            return ssh;
+        }
+		
+        return ssh_copy;
     }
 
     private String launchCommandIn(ArgumentListBuilder args, File workDir) throws GitException, InterruptedException {
@@ -1964,10 +2243,18 @@ private String launchCommandIn(ArgumentListBuilder args, File workDir, EnvVars e
     	return launchCommandIn(args, workDir, environment, TIMEOUT);
     }
 
+    @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "earlier readStderr()/readStdout() call prevents null return")
+    private String readProcessIntoString(Proc process, String encoding, boolean useStderr)
+        throws IOException, UnsupportedEncodingException {
+        if (useStderr) {
+            /* process.getStderr reference is the findbugs warning to be suppressed */
+            return IOUtils.toString(process.getStderr(), encoding);
+        }
+        /* process.getStdout reference is the findbugs warning to be suppressed */
+        return IOUtils.toString(process.getStdout(), encoding);
+    }
+
     private String launchCommandIn(ArgumentListBuilder args, File workDir, EnvVars env, Integer timeout) throws GitException, InterruptedException {
-        ByteArrayOutputStream fos = new ByteArrayOutputStream();
-        // JENKINS-13356: capture the output of stderr separately
-        ByteArrayOutputStream err = new ByteArrayOutputStream();
 
         EnvVars freshEnv = new EnvVars(env);
         // If we don't have credentials, but the requested URL requires them,
@@ -1985,26 +2272,53 @@ private String launchCommandIn(ArgumentListBuilder args, File workDir, EnvVars e
                 /* GIT_SSH won't call the passphrase prompt script unless detached from controlling terminal */
                 args.prepend("setsid");
             }
-            listener.getLogger().println(" > " + command + (timeout != null ? TIMEOUT_LOG_PREFIX + timeout : ""));
-            Launcher.ProcStarter p = launcher.launch().cmds(args.toCommandArray()).
-                    envs(freshEnv).stdout(fos).stderr(err);
-            if (workDir != null) p.pwd(workDir);
-            int status = p.start().joinWithTimeout(timeout != null ? timeout : TIMEOUT, TimeUnit.MINUTES, listener);
+            int usedTimeout = timeout == null ? TIMEOUT : timeout;
+            listener.getLogger().println(" > " + command + TIMEOUT_LOG_PREFIX + usedTimeout);
+
+            Launcher.ProcStarter p = launcher.launch().cmds(args.toCommandArray()).envs(freshEnv);
+
+            if (workDir != null) {
+                p.pwd(workDir);
+            }
+
+            int status;
+            String stdout;
+            String stderr;
+
+            if (isZos()) {
+                // Another behavior on z/OS required due to the race condition happening during transcoding of charset in
+                // EBCDIC code page if CopyThread is used on IBM z/OS Java. For unclear reason, if we rely on Proc class consumption
+                // of stdout and stderr with StreamCopyThread, then first several chars of a stream aren't get transcoded.
+                // Also, there is a need to pass a EBCDIC codepage conversion charset into input stream.
+                p.readStdout().readStderr();
+                Proc process = p.start();
+
+                status = process.joinWithTimeout(usedTimeout, TimeUnit.MINUTES, listener);
+
+                stdout = readProcessIntoString(process, encoding, false);
+                stderr = readProcessIntoString(process, encoding, true);
+            } else {
+                // JENKINS-13356: capture stdout and stderr separately
+                ByteArrayOutputStream stdoutStream = new ByteArrayOutputStream();
+                ByteArrayOutputStream stderrStream = new ByteArrayOutputStream();
+
+                p.stdout(stdoutStream).stderr(stderrStream);
+                status = p.start().joinWithTimeout(usedTimeout, TimeUnit.MINUTES, listener);
+
+                stdout = stdoutStream.toString(encoding);
+                stderr = stderrStream.toString(encoding);
+            }
 
-            String result = fos.toString(Charset.defaultCharset().toString());
             if (status != 0) {
-                throw new GitException("Command \""+command+"\" returned status code " + status + ":\nstdout: " + result + "\nstderr: "+ err.toString(Charset.defaultCharset().toString()));
+                throw new GitException("Command \"" + command + "\" returned status code " + status + ":\nstdout: " + stdout + "\nstderr: "+ stderr);
             }
 
-            return result;
+            return stdout;
         } catch (GitException | InterruptedException e) {
             throw e;
-        } catch (IOException e) {
-            throw new GitException("Error performing command: " + command, e);
-        } catch (Throwable t) {
-            throw new GitException("Error performing git command", t);
+        } catch (Throwable e) {
+            throw new GitException("Error performing git command: " + command, e);
         }
-
     }
 
     /**
@@ -2014,22 +2328,25 @@ private String launchCommandIn(ArgumentListBuilder args, File workDir, EnvVars e
      */
     public PushCommand push() {
         return new PushCommand() {
-            public URIish remote;
-            public String refspec;
-            public boolean force;
-            public boolean tags;
-            public Integer timeout;
+            private URIish remote;
+            private String refspec;
+            private boolean force;
+            private boolean tags;
+            private Integer timeout;
 
+            @Override
             public PushCommand to(URIish remote) {
                 this.remote = remote;
                 return this;
             }
 
+            @Override
             public PushCommand ref(String refspec) {
                 this.refspec = refspec;
                 return this;
             }
 
+            @Override
             public PushCommand force() {
                 return force(true);
             }
@@ -2040,16 +2357,19 @@ public PushCommand force(boolean force) {
                 return this;
             }
 
+            @Override
             public PushCommand tags(boolean tags) {
                 this.tags = tags;
                 return this;
             }
 
+            @Override
             public PushCommand timeout(Integer timeout) {
                 this.timeout = timeout;
                 return this;
             }
 
+            @Override
             public void execute() throws GitException, InterruptedException {
                 ArgumentListBuilder args = new ArgumentListBuilder();
                 args.add("push", remote.toPrivateASCIIString());
@@ -2188,39 +2508,45 @@ public Set getRemoteBranches() throws GitException, InterruptedException
     public CheckoutCommand checkout() {
         return new CheckoutCommand() {
 
-            public String ref;
-            public String branch;
-            public boolean deleteBranch;
-            public List sparseCheckoutPaths = Collections.emptyList();
-            public Integer timeout;
-            public String lfsRemote;
-            public StandardCredentials lfsCredentials;
+            private String ref;
+            private String branch;
+            private boolean deleteBranch;
+            private List sparseCheckoutPaths = Collections.emptyList();
+            private Integer timeout;
+            private String lfsRemote;
+            private StandardCredentials lfsCredentials;
 
+            @Override
             public CheckoutCommand ref(String ref) {
                 this.ref = ref;
                 return this;
             }
 
+            @Override
             public CheckoutCommand branch(String branch) {
                 this.branch = branch;
                 return this;
             }
 
+            @Override
             public CheckoutCommand deleteBranchIfExist(boolean deleteBranch) {
                 this.deleteBranch = deleteBranch;
                 return this;
             }
 
+            @Override
             public CheckoutCommand sparseCheckoutPaths(List sparseCheckoutPaths) {
                 this.sparseCheckoutPaths = sparseCheckoutPaths == null ? Collections.emptyList() : sparseCheckoutPaths;
                 return this;
             }
 
+            @Override
             public CheckoutCommand timeout(Integer timeout) {
                 this.timeout = timeout;
                 return this;
             }
 
+            @Override
             public CheckoutCommand lfsRemote(String lfsRemote) {
                 this.lfsRemote = lfsRemote;
                 return this;
@@ -2244,6 +2570,7 @@ private void interruptThisCheckout() throws InterruptedException {
                 throw new InterruptedException(interruptMessage);
             }
 
+            @Override
             public void execute() throws GitException, InterruptedException {
                 /* File.lastModified() limited by file system time, several
                  * popular Linux file systems only have 1 second granularity.
@@ -2441,12 +2768,13 @@ public List lsTree(String treeIsh, boolean recursive) throws GitExce
      */
     public RevListCommand revList_() {
         return new RevListCommand() {
-            public boolean all;
-            public boolean nowalk;
-            public boolean firstParent;
-            public String refspec;
-            public List out;
+            private boolean all;
+            private boolean nowalk;
+            private boolean firstParent;
+            private String refspec;
+            private List out;
 
+            @Override
             public RevListCommand all() {
                 return all(true);
             }
@@ -2456,6 +2784,8 @@ public RevListCommand all(boolean all) {
                 this.all = all;
                 return this;
             }
+
+            @Override
             public RevListCommand nowalk(boolean nowalk) {
                 // --no-walk wasn't introduced until v1.5.3
                 if (isAtLeastVersion(1, 5, 3, 0)) {
@@ -2464,6 +2794,7 @@ public RevListCommand nowalk(boolean nowalk) {
                 return this;
             }
 
+            @Override
             public RevListCommand firstParent() {
                 return firstParent(true);
             }
@@ -2474,16 +2805,19 @@ public RevListCommand firstParent(boolean firstParent) {
                 return this;
             }
 
+            @Override
             public RevListCommand to(List revs){
                 this.out = revs;
                 return this;
             }
 
+            @Override
             public RevListCommand reference(String reference){
                 this.refspec = reference;
                 return this;
             }
 
+            @Override
             public void execute() throws GitException, InterruptedException {
                 ArgumentListBuilder args = new ArgumentListBuilder("rev-list");
 
@@ -2985,7 +3319,11 @@ public String getAllLogEntries(String branch) throws InterruptedException {
 
     /** inline ${@link hudson.Functions#isWindows()} to prevent a transient remote classloader issue */
     private boolean isWindows() {
-        return File.pathSeparatorChar==';';
+        return File.pathSeparatorChar == ';';
+    }
+
+    private boolean isZos() {
+        return File.pathSeparatorChar == ':' && System.getProperty("os.name").equals("z/OS");
     }
 
     /* Return true if setsid program exists */
diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/CloneCommand.java b/src/main/java/org/jenkinsci/plugins/gitclient/CloneCommand.java
index 6c357cd8a7..3f2a0caa91 100644
--- a/src/main/java/org/jenkinsci/plugins/gitclient/CloneCommand.java
+++ b/src/main/java/org/jenkinsci/plugins/gitclient/CloneCommand.java
@@ -109,7 +109,7 @@ public interface CloneCommand extends GitCommand {
 
     /**
      * When shallow cloning, allow for a depth to be set in cases where you need more than the immediate last commit.
-     * Has no effect if shallow is set to false (default)
+     * Has no effect if shallow is set to false (default).
      *
      * @param depth number of revisions to be included in shallow clone
      * @return a {@link org.jenkinsci.plugins.gitclient.CloneCommand} object.
diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/Git.java b/src/main/java/org/jenkinsci/plugins/gitclient/Git.java
index 3d8c616441..ead40c530a 100644
--- a/src/main/java/org/jenkinsci/plugins/gitclient/Git.java
+++ b/src/main/java/org/jenkinsci/plugins/gitclient/Git.java
@@ -117,23 +117,7 @@ public Git using(String exe) {
      * @throws java.lang.InterruptedException if interrupted.
      */
     public GitClient getClient() throws IOException, InterruptedException {
-        jenkins.MasterToSlaveFileCallable callable = new jenkins.MasterToSlaveFileCallable() {
-            public GitClient invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
-                if (listener == null) listener = TaskListener.NULL;
-                if (env == null) env = new EnvVars();
-
-                if (exe == null || JGitTool.MAGIC_EXENAME.equalsIgnoreCase(exe)) {
-                    return new JGitAPIImpl(f, listener);
-                }
-
-                if (JGitApacheTool.MAGIC_EXENAME.equalsIgnoreCase(exe)) {
-                    final PreemptiveAuthHttpClientConnectionFactory factory = new PreemptiveAuthHttpClientConnectionFactory();
-                    return new JGitAPIImpl(f, listener, factory);
-                }
-                // Ensure we return a backward compatible GitAPI, even API only claim to provide a GitClient
-                return new GitAPI(exe, f, listener, env);
-            }
-        };
+        jenkins.MasterToSlaveFileCallable callable = new GitAPIMasterToSlaveFileCallable();
         GitClient git = (repository!=null ? repository.act(callable) : callable.invoke(null,null));
         Jenkins jenkinsInstance = Jenkins.getInstance();
         if (jenkinsInstance != null && git != null)
@@ -151,4 +135,22 @@ public GitClient invoke(File f, VirtualChannel channel) throws IOException, Inte
     public static final boolean USE_CLI = Boolean.valueOf(System.getProperty(Git.class.getName() + ".useCLI", "true"));
 
     private static final long serialVersionUID = 1L;
+
+    private class GitAPIMasterToSlaveFileCallable extends jenkins.MasterToSlaveFileCallable {
+        public GitClient invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
+            if (listener == null) listener = TaskListener.NULL;
+            if (env == null) env = new EnvVars();
+
+            if (exe == null || JGitTool.MAGIC_EXENAME.equalsIgnoreCase(exe)) {
+                return new JGitAPIImpl(f, listener);
+            }
+
+            if (JGitApacheTool.MAGIC_EXENAME.equalsIgnoreCase(exe)) {
+                final PreemptiveAuthHttpClientConnectionFactory factory = new PreemptiveAuthHttpClientConnectionFactory();
+                return new JGitAPIImpl(f, listener, factory);
+            }
+            // Ensure we return a backward compatible GitAPI, even API only claim to provide a GitClient
+            return new GitAPI(exe, f, listener, env);
+        }
+    }
 }
diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/GitClient.java b/src/main/java/org/jenkinsci/plugins/gitclient/GitClient.java
index f4650e79d3..f3a412bd5e 100644
--- a/src/main/java/org/jenkinsci/plugins/gitclient/GitClient.java
+++ b/src/main/java/org/jenkinsci/plugins/gitclient/GitClient.java
@@ -49,7 +49,6 @@ public interface GitClient {
     CredentialsMatcher CREDENTIALS_MATCHER = CredentialsMatchers.anyOf(
             CredentialsMatchers.instanceOf(StandardUsernamePasswordCredentials.class),
             CredentialsMatchers.instanceOf(SSHUserPrivateKey.class)
-            // TODO does anyone use SSL client certificates with GIT?
     );
 
     /**
@@ -232,7 +231,7 @@ public interface GitClient {
 
     /**
      * Checks out the specified commit/tag/branch into the workspace.
-     * (equivalent of git checkout branch.)
+     * (equivalent of git checkout branch.)
      *
      * @param ref A git object references expression (either a sha1, tag or branch)
      * @deprecated use {@link #checkout()} and {@link org.jenkinsci.plugins.gitclient.CheckoutCommand}
@@ -247,7 +246,7 @@ public interface GitClient {
      *
      * This will fail if the branch already exists.
      *
-     * @param ref A git object references expression. For backward compatibility, null will checkout current HEAD
+     * @param ref A git object references expression. For backward compatibility, null will checkout current HEAD
      * @param branch name of the branch to create from reference
      * @deprecated use {@link #checkout()} and {@link org.jenkinsci.plugins.gitclient.CheckoutCommand}
      * @throws hudson.plugins.git.GitException if underlying git operation fails.
@@ -269,7 +268,7 @@ public interface GitClient {
      *
      * 
    *
  • The branch of the specified name branch exists and points to the specified ref - *
  • HEAD points to branch. IOW, the workspace is on the specified branch. + *
  • HEAD points to branch. In other words, the workspace is on the specified branch. *
  • Both index and workspace are the same tree with ref. * (no dirty files and no staged changes, although this method will not touch untracked files * in the workspace.) @@ -277,7 +276,7 @@ public interface GitClient { * *

    * This method is preferred over the {@link #checkout(String, String)} family of methods, as - * this method is affected far less by the current state of the repository. The checkout + * this method is affected far less by the current state of the repository. The checkout * methods, in their attempt to emulate the "git checkout" command line behaviour, have too many * side effects. In Jenkins, where you care a lot less about throwing away local changes and * care a lot more about resetting the workspace into a known state, methods like this is more useful. @@ -298,7 +297,7 @@ public interface GitClient { * Clone a remote repository * * @param url URL for remote repository to clone - * @param origin upstream track name, defaults to origin by convention + * @param origin upstream track name, defaults to origin by convention * @param useShallowClone option to create a shallow clone, that has some restriction but will make clone operation * @param reference (optional) reference to a local clone for faster clone operations (reduce network and local storage costs) * @throws hudson.plugins.git.GitException if underlying git operation fails. @@ -315,7 +314,7 @@ public interface GitClient { /** * Fetch commits from url which match any of the passed in - * refspecs. Assumes remote.remoteName.url has been set. + * refspecs. Assumes remote.remoteName.url has been set. * * @deprecated use {@link #fetch_()} and configure a {@link org.jenkinsci.plugins.gitclient.FetchCommand} * @param url a {@link org.eclipse.jgit.transport.URIish} object. @@ -435,6 +434,18 @@ public interface GitClient { */ void clean() throws GitException, InterruptedException; + /** + * Fully revert working copy to a clean state, i.e. run both + * git-reset(1) --hard then + * git-clean(1) for working copy to + * match a fresh clone. + * + * @param cleanSubmodule flag to add extra -f + * @throws hudson.plugins.git.GitException if underlying git operation fails. + * @throws java.lang.InterruptedException if interrupted. + */ + void clean(boolean cleanSubmodule) throws GitException, InterruptedException; + // --- manage branches @@ -479,7 +490,7 @@ public interface GitClient { // --- manage tags /** - * Create (or update) a tag. If tag already exist it gets updated (equivalent to git tag --force) + * Create (or update) a tag. If tag already exist it gets updated (equivalent to git tag --force) * * @param tagName a {@link java.lang.String} object. * @param comment a {@link java.lang.String} object. @@ -540,7 +551,7 @@ public interface GitClient { // --- manage refs /** - * Create (or update) a ref. The ref will reference HEAD (equivalent to git update-ref ... HEAD). + * Create (or update) a ref. The ref will reference HEAD (equivalent to git update-ref ... HEAD). * * @param refName the full name of the ref (e.g. "refs/myref"). Spaces will be replaced with underscores. * @throws hudson.plugins.git.GitException if underlying git operation fails. @@ -549,7 +560,7 @@ public interface GitClient { void ref(String refName) throws GitException, InterruptedException; /** - * Check if a ref exists. Equivalent to comparing the return code of git show-ref to zero. + * Check if a ref exists. Equivalent to comparing the return code of git show-ref to zero. * * @param refName the full name of the ref (e.g. "refs/myref"). Spaces will be replaced with underscores. * @return True if the ref exists, false otherwse. @@ -559,7 +570,7 @@ public interface GitClient { boolean refExists(String refName) throws GitException, InterruptedException; /** - * Deletes a ref. Has no effect if the ref does not exist, equivalent to git update-ref -d. + * Deletes a ref. Has no effect if the ref does not exist, equivalent to git update-ref -d. * * @param refName the full name of the ref (e.g. "refs/myref"). Spaces will be replaced with underscores. * @throws hudson.plugins.git.GitException if underlying git operation fails. @@ -568,7 +579,7 @@ public interface GitClient { void deleteRef(String refName) throws GitException, InterruptedException; /** - * List refs with the given prefix. Equivalent to git for-each-ref --format="%(refname)". + * List refs with the given prefix. Equivalent to git for-each-ref --format="%(refname)". * * @param refPrefix the literal prefix any ref returned will have. The empty string implies all. * @return a set of refs, each beginning with the given prefix. Empty if none. @@ -601,7 +612,7 @@ public interface GitClient { ObjectId getHeadRev(String remoteRepoUrl, String branch) throws GitException, InterruptedException; /** - * List references in a remote repository. Equivalent to git ls-remote [--heads] [--tags] <repository> [<refs>]. + * List references in a remote repository. Equivalent to git ls-remote [--heads] [--tags] <repository> [<refs>]. * * @param remoteRepoUrl * Remote repository URL. @@ -620,8 +631,8 @@ public interface GitClient { Map getRemoteReferences(String remoteRepoUrl, String pattern, boolean headsOnly, boolean tagsOnly) throws GitException, InterruptedException; /** - * List symbolic references in a remote repository. Equivalent to git ls-remote --symref <repository> - * [<refs>]. Note: the response may be empty for multiple reasons + * List symbolic references in a remote repository. Equivalent to git ls-remote --symref <repository> + * [<refs>]. Note: the response may be empty for multiple reasons * * @param remoteRepoUrl Remote repository URL. * @param pattern Only references matching the given pattern are displayed. @@ -634,7 +645,7 @@ public interface GitClient { Map getRemoteSymbolicReferences(String remoteRepoUrl, String pattern) throws GitException, InterruptedException; /** - * Retrieve commit object that is direct child for revName revision reference. + * Retrieve commit object that is direct child for revName revision reference. * * @param revName a commit sha1 or tag/branch refname * @throws hudson.plugins.git.GitException when no such commit / revName is found in repository. @@ -711,7 +722,7 @@ public interface GitClient { /** * Run submodule update optionally recursively on all submodules - * (equivalent of git submodule update --recursive.) + * (equivalent of git submodule update --recursive.) * * @deprecated use {@link #submoduleUpdate()} and {@link org.jenkinsci.plugins.gitclient.SubmoduleUpdateCommand} * @param recursive a boolean. @@ -723,7 +734,7 @@ public interface GitClient { /** * Run submodule update optionally recursively on all submodules, with a specific * reference passed to git clone if needing to --init. - * (equivalent of git submodule update --recursive --reference 'reference'.) + * (equivalent of git submodule update --recursive --reference 'reference'.) * * @deprecated use {@link #submoduleUpdate()} and {@link org.jenkinsci.plugins.gitclient.SubmoduleUpdateCommand} * @param recursive a boolean. @@ -735,7 +746,7 @@ public interface GitClient { /** * Run submodule update optionally recursively on all submodules, optionally with remoteTracking submodules - * (equivalent of git submodule update --recursive --remote.) + * (equivalent of git submodule update --recursive --remote.) * * @deprecated use {@link #submoduleUpdate()} and {@link org.jenkinsci.plugins.gitclient.SubmoduleUpdateCommand} * @param recursive a boolean. @@ -747,7 +758,7 @@ public interface GitClient { /** * Run submodule update optionally recursively on all submodules, optionally with remoteTracking, with a specific * reference passed to git clone if needing to --init. - * (equivalent of git submodule update --recursive --remote --reference 'reference'.) + * (equivalent of git submodule update --recursive --remote --reference 'reference'.) * * @deprecated use {@link #submoduleUpdate()} and {@link org.jenkinsci.plugins.gitclient.SubmoduleUpdateCommand} * @param recursive a boolean. @@ -876,7 +887,7 @@ public interface GitClient { * For merge commit, this method reports one diff per each parent. This makes this method * behave differently from {@link #changelog()}. * - * @return The git show output, in raw format. + * @return The git show output, in raw format. * @param from a {@link org.eclipse.jgit.lib.ObjectId} object. * @param to a {@link org.eclipse.jgit.lib.ObjectId} object. * @throws hudson.plugins.git.GitException if underlying git operation fails. @@ -900,7 +911,7 @@ public interface GitClient { * For merge commit, this method reports one diff per each parent. This makes this method * behave differently from {@link #changelog()}. * - * @return The git show output, in raw format. + * @return The git show output, in raw format. * @param from a {@link org.eclipse.jgit.lib.ObjectId} object. * @param to a {@link org.eclipse.jgit.lib.ObjectId} object. * @param useRawOutput a {java.lang.Boolean} object. diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java index a9f0038e25..3feda708eb 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java @@ -21,7 +21,6 @@ import hudson.plugins.git.GitLockFailedException; import hudson.plugins.git.IndexEntry; import hudson.plugins.git.Revision; -import hudson.util.IOUtils; import java.io.File; import java.io.FileNotFoundException; @@ -30,15 +29,12 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.io.Writer; -import java.nio.file.Files; -import java.nio.file.Paths; import java.lang.reflect.Field; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -50,6 +46,7 @@ import javax.annotation.Nullable; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang.time.FastDateFormat; import org.eclipse.jgit.api.AddNoteCommand; import org.eclipse.jgit.api.CommitCommand; @@ -175,16 +172,19 @@ public class JGitAPIImpl extends LegacyCompatibleGitAPIImpl { /** * clearCredentials. */ + @Override public void clearCredentials() { asSmartCredentialsProvider().clearCredentials(); } /** {@inheritDoc} */ + @Override public void addCredentials(String url, StandardCredentials credentials) { asSmartCredentialsProvider().addCredentials(url, credentials); } /** {@inheritDoc} */ + @Override public void addDefaultCredentials(StandardCredentials credentials) { asSmartCredentialsProvider().addDefaultCredentials(credentials); } @@ -210,16 +210,19 @@ private synchronized CredentialsProvider getProvider() { } /** {@inheritDoc} */ + @Override public GitClient subGit(String subdir) { return new JGitAPIImpl(new File(workspace, subdir), listener); } /** {@inheritDoc} */ + @Override public void setAuthor(String name, String email) throws GitException { author = new PersonIdent(name,email); } /** {@inheritDoc} */ + @Override public void setCommitter(String name, String email) throws GitException { committer = new PersonIdent(name,email); } @@ -230,6 +233,7 @@ public void setCommitter(String name, String email) throws GitException { * @throws hudson.plugins.git.GitException if underlying git operation fails. * @throws java.lang.InterruptedException if interrupted. */ + @Override public void init() throws GitException, InterruptedException { init_().workspace(workspace.getAbsolutePath()).execute(); } @@ -247,34 +251,39 @@ private void doInit(String workspace, boolean bare) throws GitException { * * @return a {@link org.jenkinsci.plugins.gitclient.CheckoutCommand} object. */ + @Override public CheckoutCommand checkout() { return new CheckoutCommand() { + private String ref; + private String branch; + private boolean deleteBranch; + private List sparseCheckoutPaths = Collections.emptyList(); - public String ref; - public String branch; - public boolean deleteBranch; - public List sparseCheckoutPaths = Collections.emptyList(); - + @Override public CheckoutCommand ref(String ref) { this.ref = ref; return this; } + @Override public CheckoutCommand branch(String branch) { this.branch = branch; return this; } + @Override public CheckoutCommand deleteBranchIfExist(boolean deleteBranch) { this.deleteBranch = deleteBranch; return this; } + @Override public CheckoutCommand sparseCheckoutPaths(List sparseCheckoutPaths) { this.sparseCheckoutPaths = sparseCheckoutPaths == null ? Collections.emptyList() : sparseCheckoutPaths; return this; } + @Override public CheckoutCommand timeout(Integer timeout) { // noop in jgit return this; @@ -295,6 +304,7 @@ public CheckoutCommand lfsCredentials(StandardCredentials lfsCredentials) { return lfsCheckoutIsNotSupported(); } + @Override public void execute() throws GitException, InterruptedException { if(! sparseCheckoutPaths.isEmpty()) { @@ -303,16 +313,16 @@ public void execute() throws GitException, InterruptedException { } if (branch == null) - doCheckout(ref); + doCheckoutWithResetAndRetry(ref); else if (deleteBranch) - doCheckoutCleanBranch(branch, ref); + doCheckoutWithResetAndRetryAndCleanBranch(branch, ref); else doCheckout(ref, branch); } }; } - private void doCheckout(String ref) throws GitException { + private void doCheckoutWithResetAndRetry(String ref) throws GitException { boolean retried = false; Repository repo = null; while (true) { @@ -344,7 +354,7 @@ private void doCheckout(String ref) throws GitException { for (String remote : repo.getRemoteNames()) { // look for exactly ONE remote tracking branch String matchingRemoteBranch = Constants.R_REMOTES + remote + "/" + ref; - if (repo.getRef(matchingRemoteBranch) != null) { + if (repo.exactRef(matchingRemoteBranch) != null) { remoteTrackingBranches.add(matchingRemoteBranch); } } @@ -400,14 +410,13 @@ private void doCheckout(String ref) throws GitException { private void doCheckout(String ref, String branch) throws GitException { try (Repository repo = getRepository()) { - if (ref == null) ref = repo.resolve(HEAD).name(); git(repo).checkout().setName(branch).setCreateBranch(true).setForce(true).setStartPoint(ref).call(); - } catch (IOException | GitAPIException e) { + } catch (GitAPIException e) { throw new GitException("Could not checkout " + branch + " with start point " + ref, e); } } - private void doCheckoutCleanBranch(String branch, String ref) throws GitException { + private void doCheckoutWithResetAndRetryAndCleanBranch(String branch, String ref) throws GitException { try (Repository repo = getRepository()) { RefUpdate refUpdate = repo.updateRef(R_HEADS + branch); refUpdate.setNewObjectId(repo.resolve(ref)); @@ -421,7 +430,7 @@ private void doCheckoutCleanBranch(String branch, String ref) throws GitExceptio throw new GitException("Could not update " + branch + " to " + ref); } - doCheckout(branch); + doCheckoutWithResetAndRetry(branch); } catch (IOException e) { throw new GitException("Could not checkout " + branch + " with start point " + ref, e); } @@ -429,6 +438,7 @@ private void doCheckoutCleanBranch(String branch, String ref) throws GitExceptio /** {@inheritDoc} */ + @Override public void add(String filePattern) throws GitException { try (Repository repo = getRepository()) { git(repo).add().addFilepattern(filePattern).call(); @@ -442,11 +452,10 @@ private Git git(Repository repo) { } /** {@inheritDoc} */ + @Override public void commit(String message) throws GitException { try (Repository repo = getRepository()) { - CommitCommand cmd = git(repo).commit().setMessage(message); - if (author!=null) - cmd.setAuthor(author); + CommitCommand cmd = git(repo).commit().setMessage(message).setAuthor(author); if (committer!=null) cmd.setCommitter(new PersonIdent(committer,new Date())); cmd.call(); @@ -456,6 +465,7 @@ public void commit(String message) throws GitException { } /** {@inheritDoc} */ + @Override public void branch(String name) throws GitException { try (Repository repo = getRepository()) { git(repo).branchCreate().setName(name).call(); @@ -465,6 +475,7 @@ public void branch(String name) throws GitException { } /** {@inheritDoc} */ + @Override public void deleteBranch(String name) throws GitException { try (Repository repo = getRepository()) { git(repo).branchDelete().setForce(true).setBranchNames(name).call(); @@ -479,17 +490,9 @@ public void deleteBranch(String name) throws GitException { * @return a {@link java.util.Set} object. * @throws hudson.plugins.git.GitException if underlying git operation fails. */ + @Override public Set getBranches() throws GitException { - try (Repository repo = getRepository()) { - List refs = git(repo).branchList().setListMode(ListBranchCommand.ListMode.ALL).call(); - Set branches = new HashSet<>(refs.size()); - for (Ref ref : refs) { - branches.add(new Branch(ref)); - } - return branches; - } catch (GitAPIException e) { - throw new GitException(e); - } + return getBranchesInternal(ListBranchCommand.ListMode.ALL); } /** @@ -498,9 +501,14 @@ public Set getBranches() throws GitException { * @return a {@link java.util.Set} object. * @throws hudson.plugins.git.GitException if underlying git operation fails. */ + @Override public Set getRemoteBranches() throws GitException { + return getBranchesInternal(ListBranchCommand.ListMode.REMOTE); + } + + public Set getBranchesInternal(ListBranchCommand.ListMode mode) throws GitException { try (Repository repo = getRepository()) { - List refs = git(repo).branchList().setListMode(ListBranchCommand.ListMode.REMOTE).call(); + List refs = git(repo).branchList().setListMode(mode).call(); Set branches = new HashSet<>(refs.size()); for (Ref ref : refs) { branches.add(new Branch(ref)); @@ -512,6 +520,7 @@ public Set getRemoteBranches() throws GitException { } /** {@inheritDoc} */ + @Override public void tag(String name, String message) throws GitException { try (Repository repo = getRepository()) { git(repo).tag().setName(name).setMessage(message).setForceUpdate(true).call(); @@ -521,9 +530,10 @@ public void tag(String name, String message) throws GitException { } /** {@inheritDoc} */ + @Override public boolean tagExists(String tagName) throws GitException { try (Repository repo = getRepository()) { - Ref tag = repo.getRefDatabase().getRef(R_TAGS + tagName); + Ref tag = repo.exactRef(R_TAGS + tagName); return tag != null; } catch (IOException e) { throw new GitException(e); @@ -535,21 +545,22 @@ public boolean tagExists(String tagName) throws GitException { * * @return a {@link org.jenkinsci.plugins.gitclient.FetchCommand} object. */ + @Override public org.jenkinsci.plugins.gitclient.FetchCommand fetch_() { return new org.jenkinsci.plugins.gitclient.FetchCommand() { - public URIish url; - public List refspecs; - // JGit 3.3.0 thru 3.6.0 prune more branches than expected - // Refer to GitAPITestCase.test_fetch_with_prune() + private URIish url; + private List refspecs; private boolean shouldPrune = false; - public boolean tags = true; + private boolean tags = true; + @Override public org.jenkinsci.plugins.gitclient.FetchCommand from(URIish remote, List refspecs) { this.url = remote; this.refspecs = refspecs; return this; } + @Override public org.jenkinsci.plugins.gitclient.FetchCommand prune() { return prune(true); } @@ -560,6 +571,7 @@ public org.jenkinsci.plugins.gitclient.FetchCommand prune(boolean prune) { return this; } + @Override public org.jenkinsci.plugins.gitclient.FetchCommand shallow(boolean shallow) { if (shallow) { listener.getLogger().println("[WARNING] JGit doesn't support shallow clone. This flag is ignored"); @@ -567,67 +579,50 @@ public org.jenkinsci.plugins.gitclient.FetchCommand shallow(boolean shallow) { return this; } + @Override public org.jenkinsci.plugins.gitclient.FetchCommand timeout(Integer timeout) { // noop in jgit return this; } + @Override public org.jenkinsci.plugins.gitclient.FetchCommand tags(boolean tags) { this.tags = tags; return this; } + @Override public org.jenkinsci.plugins.gitclient.FetchCommand depth(Integer depth) { listener.getLogger().println("[WARNING] JGit doesn't support shallow clone and therefore depth is meaningless. This flag is ignored"); return this; } + @Override public void execute() throws GitException, InterruptedException { try (Repository repo = getRepository()) { Git git = git(repo); List allRefSpecs = new ArrayList<>(); - if (tags) { - // see http://stackoverflow.com/questions/14876321/jgit-fetch-dont-update-tag - allRefSpecs.add(new RefSpec("+refs/tags/*:refs/tags/*")); - } if (refspecs != null) for (RefSpec rs: refspecs) if (rs != null) allRefSpecs.add(rs); - if (shouldPrune) { - // since prune is broken in JGit, we go the trivial way: - // delete all refs matching the right side of the refspecs - // then fetch and let git recreate them. - List refs = git.branchList().setListMode(ListBranchCommand.ListMode.REMOTE).call(); - - List toDelete = new ArrayList<>(refs.size()); - - for (ListIterator it = refs.listIterator(); it.hasNext(); ) { - Ref branchRef = it.next(); - if (!branchRef.isSymbolic()) { // Don't delete HEAD and other symbolic refs - for (RefSpec rs : allRefSpecs) { - if (rs.matchDestination(branchRef)) { - toDelete.add(branchRef.getName()); - break; - } - } - } - } - if (!toDelete.isEmpty()) { - // we need force = true because usually not all remote branches will be merged into the current branch. - git.branchDelete().setForce(true).setBranchNames(toDelete.toArray(new String[toDelete.size()])).call(); - } - } - FetchCommand fetch = git.fetch(); fetch.setTagOpt(tags ? TagOpt.FETCH_TAGS : TagOpt.NO_TAGS); + /* JGit 4.5 required a work around that the tags refspec had to be passed in addition to setting + * the FETCH_TAGS tagOpt. JGit 4.9.0 fixed that bug. + * However, JGit 4.9 and later will not accept an empty refspec. + * If the refspec is empty and tag fetch is requested, must add the tags refspec to fetch. + */ + if (allRefSpecs.isEmpty() && tags) { + allRefSpecs.add(new RefSpec("+refs/tags/*:refs/tags/*")); + } fetch.setRemote(url.toString()); fetch.setCredentialsProvider(getProvider()); fetch.setRefSpecs(allRefSpecs); - // fetch.setRemoveDeletedRefs(shouldPrune); + fetch.setRemoveDeletedRefs(shouldPrune); fetch.call(); } catch (GitAPIException e) { @@ -645,20 +640,20 @@ public void execute() throws GitException, InterruptedException { * @throws hudson.plugins.git.GitException if any. * @throws java.lang.InterruptedException if any. */ + @Override public void fetch(URIish url, List refspecs) throws GitException, InterruptedException { fetch_().from(url, refspecs).execute(); } /** {@inheritDoc} */ + @Override public void fetch(String remoteName, RefSpec... refspec) throws GitException { try (Repository repo = getRepository()) { FetchCommand fetch = git(repo).fetch().setTagOpt(TagOpt.FETCH_TAGS); if (remoteName != null) fetch.setRemote(remoteName); fetch.setCredentialsProvider(getProvider()); - // see http://stackoverflow.com/questions/14876321/jgit-fetch-dont-update-tag List refSpecs = new ArrayList<>(); - refSpecs.add(new RefSpec("+refs/tags/*:refs/tags/*")); if (refspec != null && refspec.length > 0) for (RefSpec rs: refspec) if (rs != null) @@ -672,16 +667,18 @@ public void fetch(String remoteName, RefSpec... refspec) throws GitException { } /** {@inheritDoc} */ + @Override public void fetch(String remoteName, RefSpec refspec) throws GitException { fetch(remoteName, new RefSpec[] {refspec}); } /** {@inheritDoc} */ + @Override public void ref(String refName) throws GitException, InterruptedException { refName = refName.replace(' ', '_'); try (Repository repo = getRepository()) { RefUpdate refUpdate = repo.updateRef(refName); - refUpdate.setNewObjectId(repo.getRef(Constants.HEAD).getObjectId()); + refUpdate.setNewObjectId(repo.exactRef(Constants.HEAD).getObjectId()); switch (refUpdate.forceUpdate()) { case NOT_ATTEMPTED: case LOCK_FAILURE: @@ -697,10 +694,11 @@ public void ref(String refName) throws GitException, InterruptedException { } /** {@inheritDoc} */ + @Override public boolean refExists(String refName) throws GitException, InterruptedException { refName = refName.replace(' ', '_'); try (Repository repo = getRepository()) { - Ref ref = repo.getRefDatabase().getRef(refName); + Ref ref = repo.findRef(refName); return ref != null; } catch (IOException e) { throw new GitException("Error checking ref " + refName, e); @@ -708,12 +706,13 @@ public boolean refExists(String refName) throws GitException, InterruptedExcepti } /** {@inheritDoc} */ + @Override public void deleteRef(String refName) throws GitException, InterruptedException { refName = refName.replace(' ', '_'); try (Repository repo = getRepository()) { RefUpdate refUpdate = repo.updateRef(refName); // Required, even though this is a forced delete. - refUpdate.setNewObjectId(repo.getRef(Constants.HEAD).getObjectId()); + refUpdate.setNewObjectId(repo.exactRef(Constants.HEAD).getObjectId()); refUpdate.setForceUpdate(true); switch (refUpdate.delete()) { case NOT_ATTEMPTED: @@ -730,6 +729,7 @@ public void deleteRef(String refName) throws GitException, InterruptedException } /** {@inheritDoc} */ + @Override public Set getRefNames(String refPrefix) throws GitException, InterruptedException { if (refPrefix.isEmpty()) { refPrefix = RefDatabase.ALL; @@ -737,10 +737,9 @@ public Set getRefNames(String refPrefix) throws GitException, Interrupte refPrefix = refPrefix.replace(' ', '_'); } try (Repository repo = getRepository()) { - Map refList = repo.getRefDatabase().getRefs(refPrefix); - // The key set for refList will have refPrefix removed, so to recover it we just grab the full name. + List refList = repo.getRefDatabase().getRefsByPrefix(refPrefix); Set refs = new HashSet<>(refList.size()); - for (Ref ref : refList.values()) { + for (Ref ref : refList) { refs.add(ref.getName()); } return refs; @@ -750,23 +749,13 @@ public Set getRefNames(String refPrefix) throws GitException, Interrupte } /** {@inheritDoc} */ + @Override public Map getHeadRev(String url) throws GitException, InterruptedException { - Map heads = new HashMap<>(); - try (Repository repo = openDummyRepository(); - final Transport tn = Transport.open(repo, new URIish(url))) { - tn.setCredentialsProvider(getProvider()); - try (FetchConnection c = tn.openFetch()) { - for (final Ref r : c.getRefs()) { - heads.put(r.getName(), r.getPeeledObjectId() != null ? r.getPeeledObjectId() : r.getObjectId()); - } - } - } catch (IOException | URISyntaxException e) { - throw new GitException(e); - } - return heads; + return getRemoteReferences(url, null, true, false); } /** {@inheritDoc} */ + @Override public Map getRemoteReferences(String url, String pattern, boolean headsOnly, boolean tagsOnly) throws GitException, InterruptedException { Map references = new HashMap<>(); @@ -797,7 +786,7 @@ public Map getRemoteReferences(String url, String pattern, boo references.put(refName, refObjectId); } } - } catch (GitAPIException | IOException e) { + } catch (JGitInternalException | GitAPIException | IOException e) { throw new GitException(e); } return references; @@ -902,6 +891,7 @@ private String replaceGlobCharsWithRegExChars(String glob) } /** {@inheritDoc} */ + @Override public ObjectId getHeadRev(String remoteRepoUrl, String branchSpec) throws GitException { try (Repository repo = openDummyRepository(); final Transport tn = Transport.open(repo, new URIish(remoteRepoUrl))) { @@ -942,6 +932,7 @@ public void close() { } /** {@inheritDoc} */ + @Override public String getRemoteUrl(String name) throws GitException { try (Repository repo = getRepository()) { return repo.getConfig().getString("remote",name,"url"); @@ -955,6 +946,7 @@ public String getRemoteUrl(String name) throws GitException { * @throws hudson.plugins.git.GitException if underlying git operation fails. */ @NonNull + @Override public Repository getRepository() throws GitException { try { return new RepositoryBuilder().setWorkTree(workspace).build(); @@ -968,11 +960,13 @@ public Repository getRepository() throws GitException { * * @return a {@link hudson.FilePath} object. */ + @Override public FilePath getWorkTree() { return new FilePath(workspace); } /** {@inheritDoc} */ + @Override public void setRemoteUrl(String name, String url) throws GitException { try (Repository repo = getRepository()) { StoredConfig config = repo.getConfig(); @@ -984,6 +978,7 @@ public void setRemoteUrl(String name, String url) throws GitException { } /** {@inheritDoc} */ + @Override public void addRemoteUrl(String name, String url) throws GitException, InterruptedException { try (Repository repo = getRepository()) { StoredConfig config = repo.getConfig(); @@ -1000,6 +995,7 @@ public void addRemoteUrl(String name, String url) throws GitException, Interrupt } /** {@inheritDoc} */ + @Override public void addNote(String note, String namespace) throws GitException { try (Repository repo = getRepository()) { ObjectId head = repo.resolve(HEAD); // commit to put a note on @@ -1035,6 +1031,7 @@ private String qualifyNotesNamespace(String namespace) { } /** {@inheritDoc} */ + @Override public void appendNote(String note, String namespace) throws GitException { try (Repository repo = getRepository()) { ObjectId head = repo.resolve(HEAD); // commit to put a note on @@ -1069,11 +1066,12 @@ public void appendNote(String note, String namespace) throws GitException { @Override public ChangelogCommand changelog() { return new ChangelogCommand() { - Repository repo = getRepository(); - ObjectReader or = repo.newObjectReader(); - RevWalk walk = new RevWalk(or); - Writer out; - boolean hasIncludedRev = false; + private Repository repo = getRepository(); + private ObjectReader or = repo.newObjectReader(); + private RevWalk walk = new RevWalk(or); + private Writer out; + private boolean hasIncludedRev = false; + private boolean listMerges = false; @Override public ChangelogCommand excludes(String rev) { @@ -1094,6 +1092,7 @@ public ChangelogCommand excludes(ObjectId rev) { } } + @Override public ChangelogCommand includes(String rev) { try { includes(repo.resolve(rev)); @@ -1157,8 +1156,7 @@ public void execute() throws GitException, InterruptedException { this.includes("HEAD"); } for (RevCommit commit : walk) { - // git whatachanged doesn't show the merge commits unless -m is given - if (commit.getParentCount()>1) continue; + if (commit.getParentCount() > 1 && !listMerges) continue; formatter.format(commit, null, pw, true); } @@ -1168,6 +1166,11 @@ public void execute() throws GitException, InterruptedException { closeResources(); } } + + public ChangelogCommand listMerges(boolean flag) { + this.listMerges = flag; + return this; + } }; } @@ -1281,57 +1284,60 @@ void format(RevCommit commit, @Nullable RevCommit parent, PrintWriter pw, Boolea /** * clean. * + * @param cleanSubmodule flag to add extra -f * @throws hudson.plugins.git.GitException if underlying git operation fails. */ - public void clean() throws GitException { + @Override + public void clean(boolean cleanSubmodule) throws GitException { try (Repository repo = getRepository()) { Git git = git(repo); git.reset().setMode(HARD).call(); - git.clean().setCleanDirectories(true).setIgnore(false).call(); + git.clean().setCleanDirectories(true).setIgnore(false).setForce(cleanSubmodule).call(); } catch (GitAPIException e) { throw new GitException(e); - // Fix JENKINS-43198: - // don't throw a "Could not delete file" if the file has actually been deleted - // See JGit bug 514434 https://bugs.eclipse.org/bugs/show_bug.cgi?id=514434 - } catch(JGitInternalException e) { - String expected = "Could not delete file "; - if (e.getMessage().startsWith(expected)) { - String path = e.getMessage().substring(expected.length()); - if (Files.exists(Paths.get(path))) { - throw e; - } // else don't throw, everything is ok. - } else { - throw e; - } } } + /** + * clean. + * + * @throws hudson.plugins.git.GitException if underlying git operation fails. + */ + @Override + public void clean() throws GitException { + this.clean(false); + } + /** * clone_. * * @return a {@link org.jenkinsci.plugins.gitclient.CloneCommand} object. */ + @Override public CloneCommand clone_() { return new CloneCommand() { - String url; - String remote = Constants.DEFAULT_REMOTE_NAME; - String reference; - Integer timeout; - boolean shared; - boolean tags = true; - List refspecs; + private String url; + private String remote = Constants.DEFAULT_REMOTE_NAME; + private String reference; + private Integer timeout; + private boolean shared; + private boolean tags = true; + private List refspecs; + @Override public CloneCommand url(String url) { this.url = url; return this; } + @Override public CloneCommand repositoryName(String name) { this.remote = name; return this; } + @Override public CloneCommand shallow() { return shallow(true); } @@ -1344,6 +1350,7 @@ public CloneCommand shallow(boolean shallow) { return this; } + @Override public CloneCommand shared() { return shared(true); } @@ -1354,36 +1361,43 @@ public CloneCommand shared(boolean shared) { return this; } + @Override public CloneCommand reference(String reference) { this.reference = reference; return this; } + @Override public CloneCommand refspecs(List refspecs) { this.refspecs = new ArrayList<>(refspecs); return this; } + @Override public CloneCommand timeout(Integer timeout) { this.timeout = timeout; return this; } + @Override public CloneCommand tags(boolean tags) { this.tags = tags; return this; } + @Override public CloneCommand noCheckout() { // this.noCheckout = true; ignored, we never do a checkout return this; } + @Override public CloneCommand depth(Integer depth) { listener.getLogger().println("[WARNING] JGit doesn't support shallow clone and therefore depth is meaningless. This flag is ignored"); return this; } + @Override public void execute() throws GitException, InterruptedException { Repository repository = null; @@ -1482,21 +1496,23 @@ else if (!referencePath.isDirectory()) * * @return a {@link org.jenkinsci.plugins.gitclient.MergeCommand} object. */ + @Override public MergeCommand merge() { return new MergeCommand() { + private ObjectId rev; + private MergeStrategy strategy; + private FastForwardMode fastForwardMode; + private boolean squash; + private boolean commit = true; + private String comment; - ObjectId rev; - MergeStrategy strategy; - FastForwardMode fastForwardMode; - boolean squash; - boolean commit = true; - String comment; - + @Override public MergeCommand setRevisionToMerge(ObjectId rev) { this.rev = rev; return this; } + @Override public MergeCommand setStrategy(MergeCommand.Strategy strategy) { if (strategy != null && !strategy.toString().isEmpty() && strategy != MergeCommand.Strategy.DEFAULT) { if (strategy == MergeCommand.Strategy.OURS) { @@ -1521,6 +1537,7 @@ public MergeCommand setStrategy(MergeCommand.Strategy strategy) { return this; } + @Override public MergeCommand setGitPluginFastForwardMode(MergeCommand.GitPluginFastForwardMode fastForwardMode) { if (fastForwardMode == MergeCommand.GitPluginFastForwardMode.FF) { this.fastForwardMode = FastForwardMode.FF; @@ -1532,21 +1549,25 @@ public MergeCommand setGitPluginFastForwardMode(MergeCommand.GitPluginFastForwar return this; } + @Override public MergeCommand setSquash(boolean squash){ this.squash = squash; return this; } + @Override public MergeCommand setMessage(String comment) { this.comment = comment; return this; } + @Override public MergeCommand setCommit(boolean commit) { this.commit = commit; return this; } + @Override public void execute() throws GitException, InterruptedException { try (Repository repo = getRepository()) { Git git = git(repo); @@ -1571,37 +1592,43 @@ public void execute() throws GitException, InterruptedException { * * @return a {@link org.jenkinsci.plugins.gitclient.InitCommand} object. */ + @Override public InitCommand init_() { return new InitCommand() { + private String workspace; + private boolean bare; - public String workspace; - public boolean bare; - + @Override public InitCommand workspace(String workspace) { this.workspace = workspace; return this; } + @Override public InitCommand bare(boolean bare) { this.bare = bare; return this; } + @Override public void execute() throws GitException, InterruptedException { doInit(workspace, bare); } }; } + @Override public RebaseCommand rebase() { return new RebaseCommand() { private String upstream; + @Override public RebaseCommand setUpstream(String upstream) { this.upstream = upstream; return this; } + @Override public void execute() throws GitException, InterruptedException { try (Repository repo = getRepository()) { Git git = git(repo); @@ -1618,6 +1645,7 @@ public void execute() throws GitException, InterruptedException { } /** {@inheritDoc} */ + @Override public void deleteTag(String tagName) throws GitException { try (Repository repo = getRepository()) { git(repo).tagDelete().setTags(tagName).call(); @@ -1627,6 +1655,7 @@ public void deleteTag(String tagName) throws GitException { } /** {@inheritDoc} */ + @Override public String getTagMessage(String tagName) throws GitException { try (Repository repo = getRepository(); ObjectReader or = repo.newObjectReader(); @@ -1638,6 +1667,7 @@ public String getTagMessage(String tagName) throws GitException { } /** {@inheritDoc} */ + @Override public List getSubmodules(String treeIsh) throws GitException { try (Repository repo = getRepository(); ObjectReader or = repo.newObjectReader(); @@ -1659,6 +1689,7 @@ public List getSubmodules(String treeIsh) throws GitException { } /** {@inheritDoc} */ + @Override public void addSubmodule(String remoteURL, String subdir) throws GitException { try (Repository repo = getRepository()) { git(repo).submoduleAdd().setPath(subdir).setURI(remoteURL).call(); @@ -1668,6 +1699,7 @@ public void addSubmodule(String remoteURL, String subdir) throws GitException { } /** {@inheritDoc} */ + @Override public Set getTagNames(String tagPattern) throws GitException { if (tagPattern == null) tagPattern = "*"; @@ -1687,6 +1719,7 @@ public Set getTagNames(String tagPattern) throws GitException { } /** {@inheritDoc} */ + @Override public Set getRemoteTagNames(String tagPattern) throws GitException { /* BUG: Lists local tag names, not remote tag names */ if (tagPattern == null) tagPattern = "*"; @@ -1694,8 +1727,8 @@ public Set getRemoteTagNames(String tagPattern) throws GitException { try (Repository repo = getRepository()) { Set tags = new HashSet<>(); FileNameMatcher matcher = new FileNameMatcher(tagPattern, '/'); - Map refList = repo.getRefDatabase().getRefs(R_TAGS); - for (Ref ref : refList.values()) { + List refList = repo.getRefDatabase().getRefsByPrefix(R_TAGS); + for (Ref ref : refList) { String name = ref.getName().substring(R_TAGS.length()); matcher.reset(); matcher.append(name); @@ -1713,6 +1746,7 @@ public Set getRemoteTagNames(String tagPattern) throws GitException { * @return true if this workspace has a git repository * @throws hudson.plugins.git.GitException if underlying git operation fails. */ + @Override public boolean hasGitRepo() throws GitException { try (Repository repo = getRepository()) { return repo.getObjectDatabase().exists(); @@ -1722,6 +1756,7 @@ public boolean hasGitRepo() throws GitException { } /** {@inheritDoc} */ + @Override public boolean isCommitInRepo(ObjectId commit) throws GitException { if (commit == null) { return false; @@ -1734,6 +1769,7 @@ public boolean isCommitInRepo(ObjectId commit) throws GitException { } /** {@inheritDoc} */ + @Override public void prune(RemoteConfig repository) throws GitException { try (Repository gitRepo = getRepository()) { String remote = repository.getName(); @@ -1777,23 +1813,27 @@ private Set listRemoteBranches(String remote) throws NotSupportedExcepti * * @return a {@link org.jenkinsci.plugins.gitclient.PushCommand} object. */ + @Override public PushCommand push() { return new PushCommand() { - public URIish remote; - public String refspec; - public boolean force; - public boolean tags; + private URIish remote; + private String refspec; + private boolean force; + private boolean tags; + @Override public PushCommand to(URIish remote) { this.remote = remote; return this; } + @Override public PushCommand ref(String refspec) { this.refspec = refspec; return this; } + @Override public PushCommand force() { return force(true); } @@ -1804,16 +1844,19 @@ public PushCommand force(boolean force) { return this; } + @Override public PushCommand tags(boolean tags) { this.tags = tags; return this; } + @Override public PushCommand timeout(Integer timeout) { // noop in jgit return this; } + @Override public void execute() throws GitException, InterruptedException { try (Repository repo = getRepository()) { RefSpec ref = (refspec != null) ? new RefSpec(fixRefSpec(repo)) : Transport.REFSPEC_PUSH_ALL; @@ -1866,7 +1909,7 @@ private String fixRefSpec(Repository repository) throws IOException { switch (spec) { default: case 0: //for the source ref. we use the repository to determine what should be pushed - Ref ref = repository.getRef(specs[spec]); + Ref ref = repository.findRef(specs[spec]); if (ref == null) { throw new IOException(String.format("Ref %s not found.", specs[spec])); } @@ -1894,15 +1937,17 @@ private String fixRefSpec(Repository repository) throws IOException { * * @return a {@link org.jenkinsci.plugins.gitclient.RevListCommand} object. */ + @Override public RevListCommand revList_() { return new RevListCommand() { - public boolean all; - public boolean nowalk; - public boolean firstParent; - public String refspec; - public List out; + private boolean all; + private boolean nowalk; + private boolean firstParent; + private String refspec; + private List out; + @Override public RevListCommand all() { return all(true); } @@ -1913,11 +1958,13 @@ public RevListCommand all(boolean all) { return this; } + @Override public RevListCommand nowalk(boolean nowalk) { this.nowalk = nowalk; return this; } + @Override public RevListCommand firstParent() { return firstParent(true); } @@ -1928,16 +1975,19 @@ public RevListCommand firstParent(boolean firstParent) { return this; } + @Override public RevListCommand to(List revs){ this.out = revs; return this; } + @Override public RevListCommand reference(String reference){ this.refspec = reference; return this; } + @Override public void execute() throws GitException, InterruptedException { if (firstParent) { throw new UnsupportedOperationException("not implemented yet"); @@ -1988,6 +2038,7 @@ else if (refspec != null) * @return a {@link java.util.List} object. * @throws hudson.plugins.git.GitException if underlying git operation fails. */ + @Override public List revListAll() throws GitException { List oidList = new ArrayList<>(); RevListCommand revListCommand = revList_(); @@ -2002,6 +2053,7 @@ public List revListAll() throws GitException { } /** {@inheritDoc} */ + @Override public List revList(String ref) throws GitException { List oidList = new ArrayList<>(); RevListCommand revListCommand = revList_(); @@ -2016,6 +2068,7 @@ public List revList(String ref) throws GitException { } /** {@inheritDoc} */ + @Override public ObjectId revParse(String revName) throws GitException { try (Repository repo = getRepository()) { ObjectId id = repo.resolve(revName + "^{commit}"); @@ -2028,11 +2081,13 @@ public ObjectId revParse(String revName) throws GitException { } /** {@inheritDoc} */ + @Override public List showRevision(ObjectId from, ObjectId to) throws GitException { return showRevision(from, to, true); } /** {@inheritDoc} */ + @Override public List showRevision(ObjectId from, ObjectId to, Boolean useRawOutput) throws GitException { try (Repository repo = getRepository(); ObjectReader or = repo.newObjectReader(); @@ -2080,6 +2135,7 @@ private Iterable submodules() throws IOException { } /** {@inheritDoc} */ + @Override public void submoduleClean(boolean recursive) throws GitException { try { for (JGitAPIImpl sub : submodules()) { @@ -2094,45 +2150,80 @@ public void submoduleClean(boolean recursive) throws GitException { } /** - * submoduleUpdate. + * Update submodules. * * @return a {@link org.jenkinsci.plugins.gitclient.SubmoduleUpdateCommand} object. */ + @Override public org.jenkinsci.plugins.gitclient.SubmoduleUpdateCommand submoduleUpdate() { return new org.jenkinsci.plugins.gitclient.SubmoduleUpdateCommand() { - boolean recursive = false; - boolean remoteTracking = false; - String ref = null; + private boolean recursive = false; + private boolean remoteTracking = false; + private String ref = null; + @Override public org.jenkinsci.plugins.gitclient.SubmoduleUpdateCommand recursive(boolean recursive) { this.recursive = recursive; return this; } + @Override public org.jenkinsci.plugins.gitclient.SubmoduleUpdateCommand remoteTracking(boolean remoteTracking) { this.remoteTracking = remoteTracking; return this; } + @Override public org.jenkinsci.plugins.gitclient.SubmoduleUpdateCommand parentCredentials(boolean parentCredentials) { // No-op for JGit implementation return this; } + @Override public org.jenkinsci.plugins.gitclient.SubmoduleUpdateCommand ref(String ref) { this.ref = ref; return this; } + @Override public org.jenkinsci.plugins.gitclient.SubmoduleUpdateCommand timeout(Integer timeout) { // noop in jgit return this; } + @Override + public org.jenkinsci.plugins.gitclient.SubmoduleUpdateCommand shallow(boolean shallow) { + if (shallow) { + listener.getLogger().println("[WARNING] JGit doesn't support shallow clone. This flag is ignored"); + } + return this; + } + + @Override + public org.jenkinsci.plugins.gitclient.SubmoduleUpdateCommand depth(Integer depth) { + listener.getLogger().println("[WARNING] JGit doesn't support shallow clone and therefore depth is meaningless. This flag is ignored"); + return this; + } + + @Override + public org.jenkinsci.plugins.gitclient.SubmoduleUpdateCommand threads(Integer threads) { + // TODO: I have no idea if JGit can update submodules in parallel + // It might work, or it might blow up horribly. This probably depends on + // whether JGit relies on any global/shared state. Since I have no + // experience with JGit, I'm leaving this unimplemented for the time + // being. But if some brave soul wants to test this, feel free to provide + // an implementation similar to the one in the CliGitAPIImpl class using + // an ExecutorService. + listener.getLogger().println("[WARNING] JGit doesn't support updating submodules in parallel. This flag is ignored"); + return this; + } + + @Override public org.jenkinsci.plugins.gitclient.SubmoduleUpdateCommand useBranch(String submodule, String branchname) { return this; } + @Override public void execute() throws GitException, InterruptedException { if (remoteTracking) { listener.getLogger().println("[ERROR] JGit doesn't support remoteTracking submodules yet."); @@ -2171,6 +2262,7 @@ public void execute() throws GitException, InterruptedException { /** {@inheritDoc} */ @Deprecated + @Override public void merge(String refSpec) throws GitException, InterruptedException { try (Repository repo = getRepository()) { merge(repo.resolve(refSpec)); @@ -2181,11 +2273,13 @@ public void merge(String refSpec) throws GitException, InterruptedException { /** {@inheritDoc} */ @Deprecated + @Override public void push(RemoteConfig repository, String refspec) throws GitException, InterruptedException { push(repository.getName(),refspec); } /** {@inheritDoc} */ + @Override public List getBranchesContaining(String revspec) throws GitException, InterruptedException { // For the reasons of backward compatibility - we do not query remote branches here. return getBranchesContaining(revspec, false); @@ -2214,6 +2308,7 @@ public List getBranchesContaining(String revspec) throws GitException, I * Since we reuse {@link RevWalk}, it'd be nice to flag commits reachable from 't' as uninteresting * and keep them across resets, but I'm not sure how to do it. */ + @Override public List getBranchesContaining(String revspec, boolean allBranches) throws GitException, InterruptedException { try (Repository repo = getRepository(); ObjectReader or = repo.newObjectReader(); @@ -2289,6 +2384,7 @@ private List getAllBranchRefs(boolean originBranches) { /** {@inheritDoc} */ @Deprecated + @Override public ObjectId mergeBase(ObjectId id1, ObjectId id2) throws InterruptedException { try (Repository repo = getRepository(); ObjectReader or = repo.newObjectReader(); @@ -2309,6 +2405,7 @@ public ObjectId mergeBase(ObjectId id1, ObjectId id2) throws InterruptedExceptio /** {@inheritDoc} */ @Deprecated + @Override public String getAllLogEntries(String branch) throws InterruptedException { try (Repository repo = getRepository(); ObjectReader or = repo.newObjectReader(); @@ -2354,6 +2451,7 @@ private void markRefs(RevWalk walk, Predicate filter) throws IOException { * @throws java.lang.InterruptedException if interrupted. */ @Deprecated + @Override public void submoduleInit() throws GitException, InterruptedException { try (Repository repo = getRepository()) { git(repo).submoduleInit().call(); @@ -2369,6 +2467,7 @@ public void submoduleInit() throws GitException, InterruptedException { * @throws java.lang.InterruptedException if interrupted. */ @Deprecated + @Override public void submoduleSync() throws GitException, InterruptedException { try (Repository repo = getRepository()) { git(repo).submoduleSync().call(); @@ -2379,6 +2478,7 @@ public void submoduleSync() throws GitException, InterruptedException { /** {@inheritDoc} */ @Deprecated + @Override public String getSubmoduleUrl(String name) throws GitException, InterruptedException { String v = null; try (Repository repo = getRepository()) { @@ -2390,6 +2490,7 @@ public String getSubmoduleUrl(String name) throws GitException, InterruptedExcep /** {@inheritDoc} */ @Deprecated + @Override public void setSubmoduleUrl(String name, String url) throws GitException, InterruptedException { try (Repository repo = getRepository()) { StoredConfig config = repo.getConfig(); @@ -2409,6 +2510,7 @@ public void setSubmoduleUrl(String name, String url) throws GitException, Interr * whoever manipulating Git. */ @Deprecated + @Override public void setupSubmoduleUrls(Revision rev, TaskListener listener) throws GitException { throw new UnsupportedOperationException("not implemented yet"); } @@ -2422,6 +2524,7 @@ public void setupSubmoduleUrls(Revision rev, TaskListener listener) throws GitEx * whoever manipulating Git. */ @Deprecated + @Override public void fixSubmoduleUrls(String remote, TaskListener listener) throws GitException, InterruptedException { throw new UnsupportedOperationException(); } @@ -2443,6 +2546,7 @@ public void fixSubmoduleUrls(String remote, TaskListener listener) throws GitExc * As we walk further and find enough tags, we go into wind-down mode and only walk * to the point of accurately determining all the depths. */ + @Override public String describe(String tip) throws GitException, InterruptedException { try (Repository repo = getRepository()) { final ObjectReader or = repo.newObjectReader(); @@ -2563,11 +2667,7 @@ public String describe(ObjectId tip) throws IOException { throw new GitException("No tags can describe "+tip); // if all the nodes are dominated by all the tags, the walk stops - Collections.sort(candidates,new Comparator() { - public int compare(Candidate o1, Candidate o2) { - return o1.depth-o2.depth; - } - }); + Collections.sort(candidates, (Candidate o1, Candidate o2) -> o1.depth-o2.depth); return candidates.get(0).describe(tipId); } catch (IOException e) { @@ -2577,6 +2677,7 @@ public int compare(Candidate o1, Candidate o2) { /** {@inheritDoc} */ @Deprecated + @Override public List lsTree(String treeIsh, boolean recursive) throws GitException, InterruptedException { try (Repository repo = getRepository(); ObjectReader or = repo.newObjectReader(); @@ -2602,6 +2703,7 @@ public List lsTree(String treeIsh, boolean recursive) throws GitExce /** {@inheritDoc} */ @Deprecated + @Override public void reset(boolean hard) throws GitException, InterruptedException { try (Repository repo = getRepository()) { ResetCommand reset = new ResetCommand(repo); @@ -2614,6 +2716,7 @@ public void reset(boolean hard) throws GitException, InterruptedException { /** {@inheritDoc} */ @Deprecated + @Override public boolean isBareRepository(String GIT_DIR) throws GitException, InterruptedException { Repository repo = null; boolean isBare = false; @@ -2641,6 +2744,7 @@ public boolean isBareRepository(String GIT_DIR) throws GitException, Interrupted /** {@inheritDoc} */ @Deprecated + @Override public String getDefaultRemote(String _default_) throws GitException, InterruptedException { Set remotes = getConfig(null).getSubsections("remote"); if (remotes.contains(_default_)) return _default_; @@ -2649,6 +2753,7 @@ public String getDefaultRemote(String _default_) throws GitException, Interrupte /** {@inheritDoc} */ @Deprecated + @Override public void setRemoteUrl(String name, String url, String GIT_DIR) throws GitException, InterruptedException { try (Repository repo = new RepositoryBuilder().setGitDir(new File(GIT_DIR)).build()) { StoredConfig config = repo.getConfig(); @@ -2661,6 +2766,7 @@ public void setRemoteUrl(String name, String url, String GIT_DIR) throws GitExce /** {@inheritDoc} */ @Deprecated + @Override public String getRemoteUrl(String name, String GIT_DIR) throws GitException, InterruptedException { return getConfig(GIT_DIR).getString("remote", name, "url"); } diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/LegacyCompatibleGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/LegacyCompatibleGitAPIImpl.java index 8d88e74b64..acf41c41b2 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/LegacyCompatibleGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/LegacyCompatibleGitAPIImpl.java @@ -194,8 +194,11 @@ public final List lsTree(String treeIsh) throws GitException, Interr /** {@inheritDoc} */ @Override - protected Object writeReplace() { - return remoteProxyFor(Channel.current().export(IGitAPI.class, this)); + protected Object writeReplace() throws java.io.ObjectStreamException { + Channel currentChannel = Channel.current(); + if (currentChannel == null) + throw new java.io.WriteAbortedException("No current channel", new java.lang.NullPointerException()); + return remoteProxyFor(currentChannel.export(IGitAPI.class, this)); } /** @@ -238,18 +241,19 @@ public List showRevision(ObjectId r) throws GitException, InterruptedExc * current use cases are not disrupted by a behavioral change. *

    * E.g. - * - * - * - * - * - * - * - * + *
    branch specnormalized
    mastermaster*
    feature1feature1*
    feature1/master
    master feature1/master*
    origin/mastermaster*
    repo2/feature1feature1*
    refs/heads/feature1refs/heads/feature1
    + * + * + * + * + * + * + * + * * - * - * - * + * + * + * *
    Branch Spec Normalization Examples
    branch specnormalized
    mastermaster*
    feature1feature1*
    feature1/master
    master feature1/master*
    origin/mastermaster*
    repo2/feature1feature1*
    refs/heads/feature1refs/heads/feature1
    origin/namespaceA/fix15
    fix15 namespaceA/fix15*
    refs/heads/namespaceA/fix15refs/heads/namespaceA/fix15
    remotes/origin/namespaceA/fix15refs/heads/namespaceA/fix15
    fix15 namespaceA/fix15*
    refs/heads/namespaceA/fix15refs/heads/namespaceA/fix15
    remotes/origin/namespaceA/fix15refs/heads/namespaceA/fix15

    * *) TODO: Normalize to "refs/heads/" * @@ -266,8 +270,8 @@ protected String extractBranchNameFromBranchSpec(String branchSpec) { } else if (branchSpec.startsWith("refs/heads/")) { branch = branchSpec; } else if (branchSpec.startsWith("refs/tags/")) { - //TODO: Discuss if tags shall be allowed. - //hudson.plugins.git.util.DefaultBuildChooser.getCandidateRevisions() in git plugin 2.0.1 explicitly allowed it. + // Tags are allowed because git plugin 2.0.1 + // DefaultBuildChooser.getCandidateRevisions() allowed them. branch = branchSpec; } else { /* Old behaviour - retained for compatibility. diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/Netrc.java b/src/main/java/org/jenkinsci/plugins/gitclient/Netrc.java index 45a4ab88c0..6b12fb3e14 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/Netrc.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/Netrc.java @@ -2,11 +2,9 @@ import edu.umd.cs.findbugs.annotations.NonNull; import hudson.plugins.git.GitException; -import hudson.util.IOUtils; import java.io.BufferedReader; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.Charset; @@ -122,20 +120,24 @@ synchronized private Netrc parse() { break; case REQ_KEY: - if ("login".equals(match)) { - state = ParseState.LOGIN; - } - else if ("password".equals(match)) { - state = ParseState.PASSWORD; - } - else if ("macdef".equals(match)) { - state = ParseState.MACDEF; - } - else if ("machine".equals(match)) { - state = ParseState.MACHINE; - } - else { + if (null == match) { state = ParseState.REQ_VALUE; + } else switch (match) { + case "login": + state = ParseState.LOGIN; + break; + case "password": + state = ParseState.PASSWORD; + break; + case "macdef": + state = ParseState.MACDEF; + break; + case "machine": + state = ParseState.MACHINE; + break; + default: + state = ParseState.REQ_VALUE; + break; } break; diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/RemoteGitImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/RemoteGitImpl.java index f386794d62..888cf4dbd6 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/RemoteGitImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/RemoteGitImpl.java @@ -143,34 +143,36 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl public void execute() throws GitException, InterruptedException { try { - channel.call(new jenkins.security.MasterToSlaveCallable() { - public Void call() throws GitException { - try { - GitCommand cmd = createCommand(); - for (Invocation inv : invocations) { - inv.replay(cmd); - } - cmd.execute(); - return null; - } catch (InvocationTargetException | IllegalAccessException | InterruptedException e) { - throw new GitException(e); - } - } - - private GitCommand createCommand() throws InvocationTargetException, IllegalAccessException { - for (Method m : GitClient.class.getMethods()) { - if (m.getReturnType()==command && m.getParameterTypes().length==0) - return command.cast(m.invoke(proxy)); - } - throw new IllegalStateException("Can't find the factory method for "+command); - } - }); + channel.call(new GitCommandMasterToSlaveCallable()); } catch (IOException e) { throw new GitException(e); } } private static final long serialVersionUID = 1L; + + private class GitCommandMasterToSlaveCallable extends jenkins.security.MasterToSlaveCallable { + public Void call() throws GitException { + try { + GitCommand cmd = createCommand(); + for (Invocation inv : invocations) { + inv.replay(cmd); + } + cmd.execute(); + return null; + } catch (InvocationTargetException | IllegalAccessException | InterruptedException e) { + throw new GitException(e); + } + } + + private GitCommand createCommand() throws InvocationTargetException, IllegalAccessException { + for (Method m : GitClient.class.getMethods()) { + if (m.getReturnType()==command && m.getParameterTypes().length==0) + return command.cast(m.invoke(proxy)); + } + throw new IllegalStateException("Can't find the factory method for "+command); + } + } } private OutputStream wrap(OutputStream os) { @@ -440,6 +442,17 @@ public void prune(RemoteConfig repository) throws GitException, InterruptedExcep proxy.prune(repository); } + /** + * clean. + * + * @param cleanSubmodule flag to add extra -f + * @throws hudson.plugins.git.GitException if underlying git operation fails. + * @throws java.lang.InterruptedException if interrupted. + */ + public void clean(boolean cleanSubmodule) throws GitException, InterruptedException { + proxy.clean(cleanSubmodule); + } + /** * clean. * diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/SubmoduleUpdateCommand.java b/src/main/java/org/jenkinsci/plugins/gitclient/SubmoduleUpdateCommand.java index ada0c10ad6..e952cba39c 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/SubmoduleUpdateCommand.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/SubmoduleUpdateCommand.java @@ -58,4 +58,30 @@ public interface SubmoduleUpdateCommand extends GitCommand { * @return a {@link org.jenkinsci.plugins.gitclient.SubmoduleUpdateCommand} object. */ SubmoduleUpdateCommand timeout(Integer timeout); + + /** + * Only clone the most recent history, not preceding history. Depth of the + * shallow clone is controlled by the #depth method. + * + * @param shallow boolean controlling whether the clone is shallow (requires git>=1.8.4) + * @return a {@link org.jenkinsci.plugins.gitclient.SubmoduleUpdateCommand} object. + */ + SubmoduleUpdateCommand shallow(boolean shallow); + + /** + * When shallow cloning, allow for a depth to be set in cases where you need more than the immediate last commit. + * Has no effect if shallow is set to false (default). + * + * @param depth number of revisions to be included in shallow clone (requires git>=1.8.4) + * @return a {@link org.jenkinsci.plugins.gitclient.SubmoduleUpdateCommand} object. + */ + SubmoduleUpdateCommand depth(Integer depth); + + /** + * Update submodules in parallel with the given number of threads. + * + * @param threads number of threads to use for updating submodules in parallel + * @return a {@link org.jenkinsci.plugins.gitclient.SubmoduleUpdateCommand} object. + */ + SubmoduleUpdateCommand threads(Integer threads); } diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/jgit/PreemptiveAuthHttpClientConnection.java b/src/main/java/org/jenkinsci/plugins/gitclient/jgit/PreemptiveAuthHttpClientConnection.java index 6c2b816a3a..6705573a99 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/jgit/PreemptiveAuthHttpClientConnection.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/jgit/PreemptiveAuthHttpClientConnection.java @@ -443,12 +443,12 @@ public boolean verify(String hostname, SSLSession session) { public void verify(String host, String[] cns, String[] subjectAlts) throws SSLException { - throw new UnsupportedOperationException(); // TODO message + throw new UnsupportedOperationException("Unsupported hostname verifier called for " + host); } public void verify(String host, X509Certificate cert) throws SSLException { - throw new UnsupportedOperationException(); // TODO message + throw new UnsupportedOperationException("Unsupported hostname verifier called for " + host + " with X.509 certificate"); } public void verify(String host, SSLSocket ssl) throws IOException { diff --git a/src/main/javadoc/overview.html b/src/main/javadoc/overview.html index 762c46223c..c24cd95964 100644 --- a/src/main/javadoc/overview.html +++ b/src/main/javadoc/overview.html @@ -5,31 +5,26 @@ -The Jenkins git client plugin provides an API to execute -general-purpose git operations on a local or remote repository. Its -primary use is from the -git-plugin; -as such, it is also used by -gerrit-plugin, -git-parameter-plugin, -workflow and cloudbees validated merge plugins. +The Jenkins git client plugin provides an API to execute general-purpose git operations on a local or remote repository. +Its primary use is from the +git plugin; +as such, it is also used by the +gerrit trigger plugin, +git parameter plugin, +and the branch source plugins (GitHub branch source, Bitbucket branch source, Gitea, and others).

    Plugin developers are encouraged to use -GitClient API in -replacement for the legacy IGitAPI. +GitClient API +instead of the legacy IGitAPI. -

    The plugin isolates this low-level git stuff from git-plugin, allowing -alternate git implementations -(like JGit).

    +

    The plugin isolates low-level git commands from the git-plugin, allowing alternate git implementations +(like JGit).

    -

    For backwards compatibility, this plugin uses API classes from the -hudson.plugins.git package.

    +

    For backwards compatibility, this plugin uses API classes from the hudson.plugins.git package.

    -

    The git client plugin also bundles JGit and JGit http server so -that callers can rely on JGit and the JGit http server being available -without including it in their own plugin packaging. This is used to -reduce the size of the packaging of the git-server plugin, and may be -useful in other plugins.

    +

    The git client plugin bundles JGit and JGit http server. +Callers can rely on JGit and the JGit http server being available without including it in their own plugin packaging. +This reduces the size dependent plugins like git-server plugin, and may be useful in other plugins.

    diff --git a/src/test/java/hudson/plugins/git/GitToolResolverTest.java b/src/test/java/hudson/plugins/git/GitToolResolverTest.java new file mode 100644 index 0000000000..db6f00b375 --- /dev/null +++ b/src/test/java/hudson/plugins/git/GitToolResolverTest.java @@ -0,0 +1,58 @@ +package hudson.plugins.git; + +import hudson.EnvVars; +import hudson.model.TaskListener; +import hudson.tools.AbstractCommandInstaller; +import hudson.tools.BatchCommandInstaller; +import hudson.tools.CommandInstaller; +import hudson.tools.InstallSourceProperty; +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +public class GitToolResolverTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + private GitTool gitTool; + + @Before + public void setUp() throws IOException { + GitTool.onLoaded(); + gitTool = GitTool.getDefaultInstallation(); + } + + @Test + public void shouldResolveToolsOnMaster() throws Exception { + final String label = "master"; + final String command = "echo Hello"; + final String toolHome = "TOOL_HOME"; + AbstractCommandInstaller installer; + String expectedSubstring; + if (isWindows()) { + installer = new BatchCommandInstaller(label, command, toolHome); + expectedSubstring = System.getProperty("java.io.tmpdir", "C:\\Temp"); + } else { + installer = new CommandInstaller(label, command, toolHome); + expectedSubstring = toolHome; + } + GitTool t = new GitTool("myGit", null, Collections.singletonList( + new InstallSourceProperty(Collections.singletonList(installer)))); + t.getDescriptor().setInstallations(t); + + GitTool defaultTool = GitTool.getDefaultInstallation(); + GitTool resolved = (GitTool) defaultTool.translate(j.jenkins, new EnvVars(), TaskListener.NULL); + assertThat(resolved.getGitExe(), org.hamcrest.CoreMatchers.containsString(expectedSubstring)); + } + + private boolean isWindows() { + return File.pathSeparatorChar == ';'; + } +} diff --git a/src/test/java/hudson/plugins/git/GitToolTest.java b/src/test/java/hudson/plugins/git/GitToolTest.java index b7d2b53c09..0d83e67a4d 100644 --- a/src/test/java/hudson/plugins/git/GitToolTest.java +++ b/src/test/java/hudson/plugins/git/GitToolTest.java @@ -11,6 +11,7 @@ import org.apache.commons.lang.SystemUtils; import org.jenkinsci.plugins.gitclient.JGitApacheTool; import org.jenkinsci.plugins.gitclient.JGitTool; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import org.junit.Before; import org.junit.ClassRule; @@ -59,8 +60,8 @@ public void testGetDescriptor() { @Test public void testGetInstallationFromDescriptor() { GitTool.DescriptorImpl descriptor = gitTool.getDescriptor(); - assertEquals(null, descriptor.getInstallation("")); - assertEquals(null, descriptor.getInstallation("not-a-valid-git-install")); + assertNull(descriptor.getInstallation("")); + assertNull(descriptor.getInstallation("not-a-valid-git-install")); } @Test @@ -69,10 +70,7 @@ public void testGetApplicableFromDescriptor() { GitTool.DescriptorImpl jgitDescriptor = (new JGitTool()).getDescriptor(); GitTool.DescriptorImpl jgitApacheDescriptor = (new JGitApacheTool()).getDescriptor(); List> toolDescriptors = gitDescriptor.getApplicableDescriptors(); - assertTrue("git tool descriptor not found in " + toolDescriptors, toolDescriptors.contains(gitDescriptor)); - assertTrue("jgit tool descriptor not found in " + toolDescriptors, toolDescriptors.contains(jgitDescriptor)); - assertTrue("jgitapache tool descriptor not found in " + toolDescriptors, toolDescriptors.contains(jgitApacheDescriptor)); - assertEquals("Wrong tool descriptor count in " + toolDescriptors, 3, toolDescriptors.size()); + assertThat(toolDescriptors, containsInAnyOrder(gitDescriptor, jgitDescriptor, jgitApacheDescriptor)); } } diff --git a/src/test/java/hudson/plugins/git/IndexEntryTest.java b/src/test/java/hudson/plugins/git/IndexEntryTest.java index d6141dfb31..6fd53e40a2 100644 --- a/src/test/java/hudson/plugins/git/IndexEntryTest.java +++ b/src/test/java/hudson/plugins/git/IndexEntryTest.java @@ -13,9 +13,6 @@ public class IndexEntryTest { private final String file = ".git" + File.separator + "index-entry-file"; private IndexEntry entry; - public IndexEntryTest() { - } - @Before public void setUp() { entry = new IndexEntry(mode, type, object, file); @@ -137,11 +134,6 @@ public void testEquals() { assertEquals(entryNull4, entryNull4a); assertEquals(entryNull4a, entryNull4); - IndexEntry entryNullA = null; - IndexEntry entryNullB = null; - assertEquals(entryNullA, entryNullB); - assertEquals(entryNullB, entryNullA); - assertNotEquals(entry, null); assertNotEquals(entry, "not an IndexEntry object"); } diff --git a/src/test/java/hudson/plugins/git/RevisionTest.java b/src/test/java/hudson/plugins/git/RevisionTest.java index 7c8f194ca8..49c18952f6 100644 --- a/src/test/java/hudson/plugins/git/RevisionTest.java +++ b/src/test/java/hudson/plugins/git/RevisionTest.java @@ -8,33 +8,33 @@ public class RevisionTest { - final String SHA1; - final ObjectId objectId; - final Revision revision1; + private final String sha1; + private final ObjectId objectId; + private final Revision revision1; - final Collection emptyCollection; - final Revision revision2; + private final Collection emptyCollection; + private final Revision revision2; - final String branchName; - final String SHA1a; - final Branch branch; - final Collection branchCollection; - final Revision revisionWithBranches; + private final String branchName; + private final String sha1a; + private final Branch branch; + private final Collection branchCollection; + private final Revision revisionWithBranches; public RevisionTest() { - this.SHA1 = "3725b67f3daa6621dd01356c96c08a1f85b90c61"; - this.objectId = ObjectId.fromString(SHA1); - this.revision1 = new Revision(objectId); - - this.emptyCollection = new ArrayList<>(); - this.revision2 = new Revision(objectId, emptyCollection); - - this.branchName = "origin/tests/getSubmodules"; - this.SHA1a = "9ac446c472a6433fe503d294ebb7d5691b590269"; - this.branch = new Branch(branchName, ObjectId.fromString(this.SHA1a)); - this.branchCollection = new ArrayList<>(); - this.branchCollection.add(this.branch); - this.revisionWithBranches = new Revision(ObjectId.fromString(this.SHA1a), branchCollection); + sha1 = "3725b67f3daa6621dd01356c96c08a1f85b90c61"; + objectId = ObjectId.fromString(sha1); + revision1 = new Revision(objectId); + + emptyCollection = new ArrayList<>(); + revision2 = new Revision(objectId, emptyCollection); + + branchName = "origin/tests/getSubmodules"; + sha1a = "9ac446c472a6433fe503d294ebb7d5691b590269"; + branch = new Branch(branchName, ObjectId.fromString(sha1a)); + branchCollection = new ArrayList<>(); + branchCollection.add(branch); + revisionWithBranches = new Revision(ObjectId.fromString(sha1a), branchCollection); } @Test @@ -42,7 +42,7 @@ public void testEquals() { assertEquals(revision1, revision1); assertNotEquals(revision1, null); assertNotEquals(null, revision1); - assertNotEquals(revision1, objectId); + assertNotEquals(objectId, revision1); assertEquals(revision1, revision2); revision2.setBranches(branchCollection); @@ -53,57 +53,55 @@ public void testEquals() { @Test public void testGetSha1() { - assertEquals(revision1.getSha1(), objectId); - assertEquals(revision2.getSha1(), objectId); + assertEquals(objectId, revision1.getSha1()); + assertEquals(objectId, revision2.getSha1()); } @Test public void testGetSha1String() { - assertEquals(revision1.getSha1String(), SHA1); - assertEquals(revision2.getSha1String(), SHA1); + assertEquals(sha1, revision1.getSha1String()); + assertEquals(sha1, revision2.getSha1String()); } @Test public void testSetSha1() { - final String newSHA1 = "b397392d6d00af263583edeaf8f7773a619d1cf8"; - final ObjectId newObjectId = ObjectId.fromString(newSHA1); + String newSha1 = "b397392d6d00af263583edeaf8f7773a619d1cf8"; + ObjectId newObjectId = ObjectId.fromString(newSha1); Revision rev = new Revision(objectId); - assertEquals(rev.getSha1(), objectId); + assertEquals(objectId, rev.getSha1()); + rev.setSha1(newObjectId); - assertEquals(rev.getSha1(), newObjectId); - assertEquals(rev.getSha1String(), newSHA1); + assertEquals(newObjectId, rev.getSha1()); + assertEquals(newSha1, rev.getSha1String()); + rev.setSha1(null); - assertEquals(rev.getSha1(), null); - assertEquals(rev.getSha1String(), ""); + assertNull(rev.getSha1()); + assertEquals("", rev.getSha1String()); } @Test public void testGetBranches() { - Collection branches = revision1.getBranches(); - assertTrue(branches.isEmpty()); + assertEquals(0, revision1.getBranches().size()); - branches = revision2.getBranches(); - assertTrue(branches.isEmpty()); + assertEquals(0, revision2.getBranches().size()); - branches = revisionWithBranches.getBranches(); - assertFalse(branches.isEmpty()); + Collection branches = revisionWithBranches.getBranches(); assertTrue(branches.contains(branch)); - assertEquals(branches.size(), 1); + assertEquals(1, branches.size()); } @Test public void testSetBranches() { - final String newSHA1 = "b397392d6d00af263583edeaf8f7773a619d1cf8"; - final ObjectId newObjectId = ObjectId.fromString(newSHA1); Revision rev = new Revision(objectId); + rev.setBranches(emptyCollection); Collection branches = rev.getBranches(); - assertTrue(branches.isEmpty()); + assertEquals(0, branches.size()); + rev.setBranches(branchCollection); branches = rev.getBranches(); - assertFalse(branches.isEmpty()); assertTrue(branches.contains(branch)); - assertEquals(branches.size(), 1); + assertEquals(1, branches.size()); } @Test @@ -119,12 +117,14 @@ public void testContainsBranchName() { String mySHA1 = "aaaaaaaa72a6433fe503d294ebb7d5691b590269"; Branch myBranch = new Branch(myBranchName, ObjectId.fromString(mySHA1)); Collection branches = new ArrayList<>(); - Revision rev = new Revision(ObjectId.fromString(this.SHA1a), branches); + Revision rev = new Revision(ObjectId.fromString(sha1a), branches); assertFalse(rev.containsBranchName(myBranchName)); + branches.add(myBranch); rev.setBranches(branches); assertTrue(rev.containsBranchName(myBranchName)); assertFalse(rev.containsBranchName(branchName)); + branches.add(branch); rev.setBranches(branches); assertTrue(rev.containsBranchName(branchName)); @@ -132,9 +132,9 @@ public void testContainsBranchName() { @Test public void testToString() { - assertEquals("Revision " + SHA1 + " ()", revision1.toString()); - assertEquals("Revision " + SHA1 + " ()", revision2.toString()); - assertEquals("Revision " + SHA1a + " (" + branchName + ")", revisionWithBranches.toString()); + assertEquals("Revision " + sha1 + " ()", revision1.toString()); + assertEquals("Revision " + sha1 + " ()", revision2.toString()); + assertEquals("Revision " + sha1a + " (" + branchName + ")", revisionWithBranches.toString()); } @Test @@ -152,18 +152,16 @@ public void testToStringNullTwoArguments() { @Test public void testClone() { Revision revision1Clone = revision1.clone(); - assertEquals(revision1Clone.getSha1(), objectId); + assertEquals(objectId, revision1Clone.getSha1()); Revision revision2Clone = revision2.clone(); - assertEquals(revision2Clone.getSha1String(), SHA1); + assertEquals(sha1, revision2Clone.getSha1String()); Revision nullRevision = new Revision(null); Revision clonedRevision = nullRevision.clone(); - assertEquals(clonedRevision, nullRevision); + assertEquals(nullRevision, clonedRevision); - Collection branches = revisionWithBranches.getBranches(); Revision revisionWithBranchesClone = revisionWithBranches.clone(); - Collection branchesCloned = revisionWithBranchesClone.getBranches(); assertTrue(revisionWithBranchesClone.containsBranchName(branchName)); } diff --git a/src/test/java/hudson/plugins/git/TagTest.java b/src/test/java/hudson/plugins/git/TagTest.java index 75fb7a1a30..6c16189ecb 100644 --- a/src/test/java/hudson/plugins/git/TagTest.java +++ b/src/test/java/hudson/plugins/git/TagTest.java @@ -8,19 +8,19 @@ import nl.jqno.equalsverifier.EqualsVerifier; public class TagTest { - private final String tagName = "git-client-1.8.1"; - private final String tagSHA1String = "3725b67f3daa6621dd01356c96c08a1f85b90c61"; - private final ObjectId tagSHA1 = ObjectId.fromString(tagSHA1String); - Tag tag; + + private Tag tag; @Before public void assignTag() { + String tagName = "git-client-1.8.1"; + ObjectId tagSHA1 = ObjectId.fromString("3725b67f3daa6621dd01356c96c08a1f85b90c61"); tag = new Tag(tagName, tagSHA1); } @Test public void testGetCommitMessage() { - assertEquals(null, tag.getCommitMessage()); + assertNull(tag.getCommitMessage()); } @Test @@ -32,7 +32,7 @@ public void testSetCommitMessage() { @Test public void testGetCommitSHA1() { - assertEquals(null, tag.getCommitSHA1()); + assertNull(tag.getCommitSHA1()); } @Test diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPIImplAuthTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPIImplAuthTest.java deleted file mode 100644 index 013db6ff70..0000000000 --- a/src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPIImplAuthTest.java +++ /dev/null @@ -1,183 +0,0 @@ -package org.jenkinsci.plugins.gitclient; - -import hudson.EnvVars; -import hudson.Launcher; -import hudson.model.TaskListener; -import hudson.util.ArgumentListBuilder; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.util.Arrays; -import java.util.Random; -import java.util.concurrent.TimeUnit; -import static org.hamcrest.Matchers.hasItems; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import static org.junit.Assert.*; - -/** - * CliGitAPIImpl authorization specific tests. - * - * @author Mark Waite - */ -public class CliGitAPIImplAuthTest { - - private final Launcher launcher; - - public CliGitAPIImplAuthTest() { - launcher = new Launcher.LocalLauncher(TaskListener.NULL); - } - - private CliGitAPIImpl git; - - private final Random random = new Random(); - - private final String[] CARET_SPECIALS = {"^", "&", "\\", "<", ">", "|", " ", "\"", "\t"}; - private final String[] PERCENT_SPECIALS = {"%"}; - - @Before - public void setUp() { - git = new CliGitAPIImpl("git", new File("."), TaskListener.NULL, new EnvVars()); - } - - @Test - public void testQuotedUsernamePasswordCredentials() throws Exception { - assertEquals("", quoteCredentials("")); - for (String special : CARET_SPECIALS) { - String expected = expectedQuoting(special); - assertEquals(expected, quoteCredentials(special)); - checkWindowsCommandOutput(special); - assertEquals(expected + expected, quoteCredentials(special + special)); - checkWindowsCommandOutput(special + special); - } - for (String special : PERCENT_SPECIALS) { - String expected = expectedQuoting(special); - assertEquals(expected, quoteCredentials(special)); - checkWindowsCommandOutput(special); - assertEquals(expected + expected, quoteCredentials(special + special)); - checkWindowsCommandOutput(special + special); - } - for (String startSpecial : CARET_SPECIALS) { - for (String endSpecial : PERCENT_SPECIALS) { - for (String middle : randomStrings()) { - String source = startSpecial + middle + endSpecial; - String expected = expectedQuoting(source); - assertEquals(expected, quoteCredentials(source)); - checkWindowsCommandOutput(source); - assertEquals(expected + expected, quoteCredentials(source + source)); - checkWindowsCommandOutput(source + source); - } - } - } - for (String startSpecial : PERCENT_SPECIALS) { - for (String endSpecial : CARET_SPECIALS) { - for (String middle : randomStrings()) { - String source = startSpecial + middle + endSpecial; - String expected = expectedQuoting(source); - assertEquals(expected, quoteCredentials(source)); - checkWindowsCommandOutput(source); - assertEquals(expected + expected, quoteCredentials(source + source)); - checkWindowsCommandOutput(source + source); - } - } - } - for (String startSpecial : PERCENT_SPECIALS) { - for (String endSpecial : PERCENT_SPECIALS) { - for (String middle : randomStrings()) { - String source = startSpecial + middle + endSpecial; - String expected = expectedQuoting(source); - assertEquals(expected, quoteCredentials(source)); - checkWindowsCommandOutput(source); - assertEquals(expected + expected, quoteCredentials(source + source)); - checkWindowsCommandOutput(source + source); - } - } - } - } - - private String quoteCredentials(String password) { - return git.escapeWindowsCharsForUnquotedString(password); - } - - private String expectedQuoting(String password) { - for (String needsCaret : CARET_SPECIALS) { - password = password.replace(needsCaret, "^" + needsCaret); - } - for (String needsPercent : PERCENT_SPECIALS) { - password = password.replace(needsPercent, "%" + needsPercent); - } - return password; - } - - private void checkWindowsCommandOutput(String password) throws Exception { - if (!isWindows() || password == null || password.trim().isEmpty()) { - /* ArgumentListBuilder can't pass spaces or tabs as arguments */ - return; - } - String userName = "git"; - File batFile = git.createWindowsBatFile(userName, password); - assertTrue(batFile.exists()); - ArgumentListBuilder args = new ArgumentListBuilder(batFile.getAbsolutePath(), "Password"); - String[] output = run(args); - assertThat(Arrays.asList(output), hasItems(password)); - if (batFile.delete() == false) { - /* Retry delete only once */ - Thread.sleep(501); /* Wait up to 0.5 second for Windows virus scanners, etc. */ - assertTrue("Failed retry of delete test batch file", batFile.delete()); - } - assertFalse(batFile.exists()); - } - - private String[] run(ArgumentListBuilder args) throws IOException, InterruptedException { - String[] output; - ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); - ByteArrayOutputStream bytesErr = new ByteArrayOutputStream(); - Launcher.ProcStarter p = launcher.launch().cmds(args).envs(new EnvVars()).stdout(bytesOut).stderr(bytesErr).pwd(new File(".")); - int status = p.start().joinWithTimeout(1, TimeUnit.MINUTES, TaskListener.NULL); - String result = bytesOut.toString("UTF-8"); - if (bytesErr.size() > 0) { - result = result + "\nstderr not empty:\n" + bytesErr.toString("UTF-8"); - } - output = result.split("[\\n\\r]"); - Assert.assertEquals(args.toString() + " command failed and reported '" + Arrays.toString(output) + "'", 0, status); - return output; - } - - /* Strings may contain ' ' but should not contain other escaped chars */ - private final String[] sourceData = { - "ЁЂЃЄЅ", - "Miloš Šafařík", - "ЌЍЎЏАБВГД", - "ЕЖЗИЙКЛМНОПРСТУФ", - "фхцчшщъыьэюя", - "الإطلاق", - "1;DROP TABLE users", - "C:", - "' OR '1'='1", - "He said, \"Hello!\", didn't he?", - "ZZ:", - "Roses are \u001b[0;31mred\u001b[0m" - }; - - private String randomString() { - int index = random.nextInt(sourceData.length); - return sourceData[index]; - } - - private String[] randomStrings() { - if (TEST_ALL_CREDENTIALS) { - return sourceData; - } - int index = random.nextInt(sourceData.length); - return new String[]{sourceData[index]}; - } - - private boolean isWindows() { - return File.pathSeparatorChar == ';'; - } - - /* If not in a Jenkins job, then default to run all credentials tests. */ - private static final String NOT_JENKINS = System.getProperty("JOB_NAME") == null ? "true" : "false"; - private static final boolean TEST_ALL_CREDENTIALS = Boolean.valueOf(System.getProperty("TEST_ALL_CREDENTIALS", NOT_JENKINS)); -} diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPIImplTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPIImplTest.java index 3c36b675de..ab45c37141 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPIImplTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPIImplTest.java @@ -57,15 +57,15 @@ protected void runTest() throws Throwable { } } - class VersionTest { + private class VersionTest { - public boolean expectedIsAtLeastVersion; - public int major; - public int minor; - public int rev; - public int bugfix; + private boolean expectedIsAtLeastVersion; + private int major; + private int minor; + private int rev; + private int bugfix; - public VersionTest(boolean assertTrueOrFalse, int major, int minor, int rev, int bugfix) { + private VersionTest(boolean assertTrueOrFalse, int major, int minor, int rev, int bugfix) { this.expectedIsAtLeastVersion = assertTrueOrFalse; this.major = major; this.minor = minor; @@ -78,20 +78,20 @@ private void doTest(String versionOutput, VersionTest[] versions) { setTimeoutVisibleInCurrentTest(false); /* No timeout for git --version command */ CliGitAPIImpl git = new CliGitAPIImpl("git", new File("."), listener, env); git.computeGitVersion(versionOutput); - for (int i = 0; i < versions.length; ++i) { - String msg = versionOutput + " for " + versions[i].major + versions[i].minor + versions[i].rev + versions[i].bugfix; - if (versions[i].expectedIsAtLeastVersion) { + for (VersionTest version : versions) { + String msg = versionOutput + " for " + version.major + version.minor + version.rev + version.bugfix; + if (version.expectedIsAtLeastVersion) { assertTrue("Failed " + msg, git.isAtLeastVersion( - versions[i].major, - versions[i].minor, - versions[i].rev, - versions[i].bugfix)); + version.major, + version.minor, + version.rev, + version.bugfix)); } else { assertFalse("Passed " + msg, git.isAtLeastVersion( - versions[i].major, - versions[i].minor, - versions[i].rev, - versions[i].bugfix)); + version.major, + version.minor, + version.rev, + version.bugfix)); } } } @@ -296,7 +296,7 @@ public void test_git_branch_with_line_breaks_and_long_strings() throws Exception setTimeoutVisibleInCurrentTest(false); CliGitAPIImpl git = new CliGitAPIImpl("git", new File("."), listener, env); Set branches = git.parseBranches(gitBranchOutput); - assertTrue("\"git branch -a -v --no-abbrev\" output correctly parsed", branches.size() == 2); + assertEquals("\"git branch -a -v --no-abbrev\" output correctly parsed", 2, branches.size()); } @Override diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPITempFileTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPITempFileTest.java index 0a74dde048..b95562bb56 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPITempFileTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPITempFileTest.java @@ -91,7 +91,7 @@ public void createWorkspace() throws Exception { * */ @Test - @Issue("JENKINS-44301") // and 43931 and ... + @Issue({"JENKINS-44301", "JENKINS-43931"}) // and ... public void testTempFilePathCharactersValid() throws IOException { CliGitAPIImplExtension cliGit = new CliGitAPIImplExtension("git", workspace, null, null); for (int charIndex = 0; charIndex < INVALID_CHARACTERS.length(); charIndex++) { @@ -116,7 +116,7 @@ private static boolean isWindows() { private class CliGitAPIImplExtension extends CliGitAPIImpl { - public CliGitAPIImplExtension(String gitExe, File workspace, TaskListener listener, EnvVars environment) { + private CliGitAPIImplExtension(String gitExe, File workspace, TaskListener listener, EnvVars environment) { super(gitExe, workspace, listener, environment); } } diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPIWindowsFilePermissionsTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPIWindowsFilePermissionsTest.java new file mode 100644 index 0000000000..37cdb0ab96 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPIWindowsFilePermissionsTest.java @@ -0,0 +1,78 @@ +package org.jenkinsci.plugins.gitclient; + +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.AclEntry; +import java.nio.file.attribute.AclEntryType; +import java.nio.file.attribute.AclFileAttributeView; +import java.nio.file.attribute.UserPrincipal; +import java.nio.file.attribute.UserPrincipalLookupService; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +public class CliGitAPIWindowsFilePermissionsTest { + + private CliGitAPIImpl cliGit; + private File file; + private AclFileAttributeView fileAttributeView; + private UserPrincipal userPrincipal; + private String username; + + @Before + public void beforeEach() throws Exception { + assumeTrue(isWindows()); + cliGit = new CliGitAPIImpl("git", new File("."), null, null); + file = cliGit.createTempFile("permission", ".suff"); + Path path = Paths.get(file.toURI()); + fileAttributeView = Files.getFileAttributeView(path, AclFileAttributeView.class); + assertNotNull(fileAttributeView); + UserPrincipalLookupService userPrincipalLookupService = path.getFileSystem().getUserPrincipalLookupService(); + assertNotNull(userPrincipalLookupService); + username = cliGit.getWindowsUserName(fileAttributeView); + assertNotNull(username); + userPrincipal = userPrincipalLookupService.lookupPrincipalByName(username); + assertNotNull(userPrincipal); + assertEquals(userPrincipal, fileAttributeView.getOwner()); + } + + @Test + public void test_windows_file_permission_is_set_correctly() throws Exception { + cliGit.fixSshKeyOnWindows(file); + assertEquals(1, fileAttributeView.getAcl().size()); + AclEntry aclEntry = fileAttributeView.getAcl().get(0); + assertTrue(aclEntry.flags().isEmpty()); + assertEquals(CliGitAPIImpl.ACL_ENTRY_PERMISSIONS, aclEntry.permissions()); + assertEquals(userPrincipal, aclEntry.principal()); + assertEquals(AclEntryType.ALLOW, aclEntry.type()); + } + + @Test + public void test_windows_file_permission_are_incorrect() throws Exception { + // By default files include System and builtin administrators + assertNotSame(1, fileAttributeView.getAcl().size()); + for (AclEntry entry : fileAttributeView.getAcl()) { + if (entry.principal().equals(userPrincipal)) { + assertNotSame(CliGitAPIImpl.ACL_ENTRY_PERMISSIONS, entry.permissions()); + } + } + } + + @Test + public void test_windows_username_lookup() { + assertEquals(username, userPrincipal.getName()); + } + + /** inline ${@link hudson.Functions#isWindows()} to prevent a transient remote classloader issue */ + private boolean isWindows() { + return File.pathSeparatorChar == ';'; + } +} diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/CliGitCommand.java b/src/test/java/org/jenkinsci/plugins/gitclient/CliGitCommand.java index 7214ebbbfd..9611291425 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/CliGitCommand.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/CliGitCommand.java @@ -1,5 +1,7 @@ package org.jenkinsci.plugins.gitclient; +import static org.junit.Assert.*; + import hudson.EnvVars; import hudson.Launcher; import hudson.model.TaskListener; @@ -10,10 +12,8 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Iterator; import java.util.List; import java.util.concurrent.TimeUnit; -import org.junit.Assert; /** * Run a command line git command, return output as array of String, optionally @@ -43,13 +43,13 @@ class CliGitCommand { } } - public String[] run(String... arguments) throws IOException, InterruptedException { + String[] run(String... arguments) throws IOException, InterruptedException { args = new ArgumentListBuilder("git"); args.add(arguments); return run(true); } - public String[] run() throws IOException, InterruptedException { + String[] run() throws IOException, InterruptedException { return run(true); } @@ -64,25 +64,20 @@ private String[] run(boolean assertProcessStatus) throws IOException, Interrupte } output = result.split("[\\n\\r]"); if (assertProcessStatus) { - Assert.assertEquals(args.toString() + " command failed and reported '" + Arrays.toString(output) + "'", 0, status); + assertEquals(args.toString() + " command failed and reported '" + Arrays.toString(output) + "'", 0, status); } return output; } - public void assertOutputContains(String... expectedRegExes) { + void assertOutputContains(String... expectedRegExes) { List notFound = new ArrayList<>(); boolean modified = notFound.addAll(Arrays.asList(expectedRegExes)); - Assert.assertTrue("Missing regular expressions in assertion", modified); + assertTrue("Missing regular expressions in assertion", modified); for (String line : output) { - for (Iterator iterator = notFound.iterator(); iterator.hasNext();) { - String regex = iterator.next(); - if (line.matches(regex)) { - iterator.remove(); - } - } + notFound.removeIf(line::matches); } if (!notFound.isEmpty()) { - Assert.fail(Arrays.toString(output) + " did not match all strings in notFound: " + Arrays.toString(expectedRegExes)); + fail(Arrays.toString(output) + " did not match all strings in notFound: " + Arrays.toString(expectedRegExes)); } } @@ -112,11 +107,8 @@ private void setConfigIfEmpty(String configName, String value) throws Exception * values assigned for user.name and user.email. This method checks the * existing values, and if they are not set, assigns default values. * If the values are already set, they are unchanged. - * - * @param userName user name to be defined (if value not already set) - * @param userEmail email address to be defined (if value not already set) */ - public void setDefaults() throws Exception { + void setDefaults() throws Exception { setConfigIfEmpty("user.name", "Vojtěch-Zweibrücken-Šafařík"); setConfigIfEmpty("user.email", "email.address.from.git.client.plugin.test@example.com"); } diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/CredentialsTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/CredentialsTest.java index ea9146b5f5..1a44a93146 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/CredentialsTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/CredentialsTest.java @@ -8,6 +8,7 @@ import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; import com.google.common.io.Files; import hudson.model.Fingerprint; +import hudson.plugins.git.GitException; import hudson.util.LogTaskListener; import hudson.util.StreamTaskListener; import java.io.File; @@ -16,6 +17,7 @@ import java.io.IOException; import java.io.PrintStream; import java.net.MalformedURLException; +import java.net.URISyntaxException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; @@ -23,20 +25,25 @@ import java.util.List; import java.util.Map; import java.util.Random; +import java.util.StringJoiner; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; -import org.apache.commons.lang.StringUtils; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.transport.RefSpec; +import org.eclipse.jgit.transport.RemoteConfig; +import org.eclipse.jgit.transport.URIish; import static org.hamcrest.Matchers.*; import org.junit.After; import static org.junit.Assert.*; +import static org.junit.Assume.*; import org.junit.Before; import org.junit.Test; import org.junit.Rule; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.json.simple.JSONObject; import org.json.simple.JSONArray; @@ -55,8 +62,8 @@ public class CredentialsTest { // Required for credentials use - @ClassRule - public static final JenkinsRule j = new JenkinsRule(); + @Rule + public final JenkinsRule j = new JenkinsRule(); private final String gitImpl; private final String gitRepoURL; @@ -73,7 +80,6 @@ public class CredentialsTest { private File repo; private StandardCredentials testedCredential; - private List expectedLogSubstrings = new ArrayList<>(); private final Random random = new Random(); @Rule @@ -96,9 +102,7 @@ public class CredentialsTest { private final static File CURR_DIR = new File("."); - private static PrintStream log() { - return StreamTaskListener.fromStdout().getLogger(); - } + private static long firstTestStartTime = 0; /* Windows refuses directory names with '*', '<', '>', '|', '?', and ':' */ private final String SPECIALS_TO_CHECK = "%()`$&{}[]" @@ -120,12 +124,9 @@ public CredentialsTest(String gitImpl, String gitRepoUrl, String username, Strin if (specialsIndex >= SPECIALS_TO_CHECK.length()) { specialsIndex = 0; } - log().println(show("Repo", gitRepoUrl) - + show("spec", specialCharacter) - + show("impl", gitImpl) - + show("user", username) - + show("pass", password) - + show("key", privateKey)); + if (firstTestStartTime == 0) { + firstTestStartTime = System.currentTimeMillis(); + } } @Before @@ -148,13 +149,7 @@ public void setUp() throws IOException, InterruptedException { logger.addHandler(handler); logger.setLevel(Level.ALL); listener = new hudson.util.LogTaskListener(logger, Level.ALL); - listener.getLogger().println(LOGGING_STARTED); git = Git.with(listener, new hudson.EnvVars()).in(repo).using(gitImpl).getClient(); - if (gitImpl.equals("git")) { - addExpectedLogSubstring("> git fetch "); - addExpectedLogSubstring("> git checkout -b master "); - } - addExpectedLogSubstring("Using reference repository: "); assertTrue("Bad username, password, privateKey combo: '" + username + "', '" + password + "'", (password == null || password.isEmpty()) ^ (privateKey == null || !privateKey.exists())); @@ -184,28 +179,6 @@ public void clearCredentials() { } } - private void checkExpectedLogSubstring() { - try { - String messages = StringUtils.join(handler.getMessages(), ";"); - assertTrue("Logging not started: " + messages, handler.containsMessageSubstring(LOGGING_STARTED)); - for (String expectedLogSubstring : expectedLogSubstrings) { - assertTrue("No '" + expectedLogSubstring + "' in " + messages, - handler.containsMessageSubstring(expectedLogSubstring)); - } - } finally { - clearExpectedLogSubstring(); - handler.close(); - } - } - - protected void addExpectedLogSubstring(String expectedLogSubstring) { - this.expectedLogSubstrings.add(expectedLogSubstring); - } - - protected void clearExpectedLogSubstring() { - this.expectedLogSubstrings = new ArrayList<>(); - } - private BasicSSHUserPrivateKey newPrivateKeyCredential(String username, File privateKey) throws IOException { CredentialsScope scope = CredentialsScope.GLOBAL; String id = "private-key-" + privateKey.getPath() + random.nextInt(); @@ -221,8 +194,7 @@ private BasicSSHUserPrivateKey newPrivateKeyCredential(String username, File pri private StandardUsernamePasswordCredentials newUsernamePasswordCredential(String username, String password) { CredentialsScope scope = CredentialsScope.GLOBAL; String id = "username-" + username + "-password-" + password + random.nextInt(); - StandardUsernamePasswordCredentials usernamePasswordCredential = new UsernamePasswordCredentialsImpl(scope, id, "desc: " + id, username, password); - return usernamePasswordCredential; + return new UsernamePasswordCredentialsImpl(scope, id, "desc: " + id, username, password); } private static boolean isCredentialsSupported() throws IOException, InterruptedException { @@ -230,6 +202,14 @@ private static boolean isCredentialsSupported() throws IOException, InterruptedE return cli.isAtLeastVersion(1, 7, 9, 0); } + private boolean isShallowCloneSupported(String implementation, GitClient gitClient) throws IOException, InterruptedException { + if (!implementation.equals("git")) { + return false; + } + CliGitAPIImpl cli = (CliGitAPIImpl) gitClient; + return cli.isAtLeastVersion(1, 9, 0, 0); + } + @Parameterized.Parameters(name = "{2}-{1}-{0}-{5}") public static Collection gitRepoUrls() throws MalformedURLException, FileNotFoundException, IOException, InterruptedException, ParseException { List repos = new ArrayList<>(); @@ -272,7 +252,7 @@ public static Collection gitRepoUrls() throws MalformedURLException, FileNotFoun if (skipIf.equals(implementation)) { continue; } - if (implementation.startsWith("jgit") && skipIf.startsWith("jgit")) { // Treat jgitapache like jgit + if (implementation.startsWith("jgit") && skipIf.equals("jgit")) { // Treat jgitapache like jgit continue; } } @@ -325,21 +305,83 @@ public static Collection gitRepoUrls() throws MalformedURLException, FileNotFoun } } Collections.shuffle(repos); // randomize test order - int toIndex = Math.min(repos.size(), TEST_ALL_CREDENTIALS ? 90 : 6); // Don't run more than 90 variations of test - about 3 minutes - return repos.subList(0, toIndex); + // If we're not testing all credentials, take 6 or less + return TEST_ALL_CREDENTIALS ? repos : repos.subList(0, Math.min(repos.size(), 6)); + } + + private void doFetch(String source) throws Exception { + /* Save some bandwidth with shallow clone for CliGit, not yet available for JGit */ + URIish sourceURI = new URIish(source); + List refSpecs = new ArrayList<>(); + refSpecs.add(new RefSpec("+refs/heads/master:refs/remotes/origin/master")); + FetchCommand cmd = git.fetch_().from(sourceURI, refSpecs).tags(false); + if (isShallowCloneSupported(gitImpl, git)) { + // Reduce network transfer by using shallow clone + // JGit does not support shallow clone + cmd.shallow(true).depth(1); + } + cmd.execute(); } - private void addCredential(String username, String password, File privateKey) throws IOException { - if (random.nextBoolean()) { - git.addDefaultCredentials(testedCredential); - } else { - git.addCredentials(gitRepoURL, testedCredential); + private String listDir(File dir) { + File[] files = repo.listFiles(); + StringJoiner joiner = new StringJoiner(","); + for (File file : files) { + joiner.add(file.getName()); } + return joiner.toString(); + } + + private void addCredential() throws IOException { + // Always use addDefaultCredentials + git.addDefaultCredentials(testedCredential); + // addCredential stops tests to prompt for passphrase + // addCredentials fails some github username / password tests + // git.addCredentials(gitRepoURL, testedCredential); + } + + /** + * Returns true if another test should be allowed to start. + * JenkinsRule test timeout defaults to 180 seconds. + * + * @return true if another test should be allowed to start + */ + private boolean testPeriodNotExpired() { + return (System.currentTimeMillis() - firstTestStartTime) < ((180 - 30) * 1000L); + } + + @Test + @Issue("JENKINS_50573") + public void testFetchWithCredentials() throws Exception { + assumeTrue(testPeriodNotExpired()); + File clonedFile = new File(repo, fileToCheck); + git.init_().workspace(repo.getAbsolutePath()).execute(); + assertFalse("file " + fileToCheck + " in " + repo + ", has " + listDir(repo), clonedFile.exists()); + addCredential(); + /* Fetch with remote URL */ + doFetch(gitRepoURL); + git.setRemoteUrl("origin", gitRepoURL); + /* Fetch with remote name "origin" instead of remote URL */ + doFetch("origin"); + ObjectId master = git.getHeadRev(gitRepoURL, "master"); + git.checkout().branch("master").ref(master.getName()).deleteBranchIfExist(true).execute(); + if (submodules) { + git.submoduleInit(); + SubmoduleUpdateCommand subcmd = git.submoduleUpdate().parentCredentials(useParentCreds); + subcmd.execute(); + } + assertTrue("master: " + master + " not in repo", git.isCommitInRepo(master)); + assertEquals("Master != HEAD", master, git.getRepository().findRef("master").getObjectId()); + assertEquals("Wrong branch", "master", git.getRepository().getBranch()); + assertTrue("No file " + fileToCheck + ", has " + listDir(repo), clonedFile.exists()); + /* prune opens a remote connection to list remote branches */ + git.prune(new RemoteConfig(git.getRepository().getConfig(), "origin")); } @Test public void testRemoteReferencesWithCredentials() throws Exception { - addCredential(username, password, privateKey); + assumeTrue(testPeriodNotExpired()); + addCredential(); Map remoteReferences; switch (random.nextInt(4)) { default: @@ -359,22 +401,11 @@ public void testRemoteReferencesWithCredentials() throws Exception { assertThat(remoteReferences.keySet(), hasItems("refs/heads/master")); } - private String show(String name, String value) { - if (value != null && !value.isEmpty()) { - return " " + name + ": '" + value + "'"; - } - return ""; - } - - private String show(String name, File file) { - if (file != null) { - return " " + name + ": '" + file.getPath() + "'"; - } - return ""; - } - - private String show(String name, char value) { - return " " + name + ": '" + value + "'"; + @Test + @Issue("JENKINS_50573") + public void isURIishRemote() throws Exception { + URIish uri = new URIish(gitRepoURL); + assertTrue("Should be remote but isn't: " + uri, uri.isRemote()); } private boolean isWindows() { diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/GitAPITestCase.java b/src/test/java/org/jenkinsci/plugins/gitclient/GitAPITestCase.java index 1ac97c2996..3443485b88 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/GitAPITestCase.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/GitAPITestCase.java @@ -8,7 +8,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import static org.jenkinsci.plugins.gitclient.StringSharesPrefix.sharesPrefix; -import static org.junit.Assert.*; import hudson.FilePath; import hudson.Launcher; @@ -18,6 +17,7 @@ import hudson.plugins.git.Branch; import hudson.plugins.git.GitException; import hudson.plugins.git.GitLockFailedException; +import hudson.plugins.git.GitObject; import hudson.plugins.git.IGitAPI; import hudson.plugins.git.IndexEntry; import hudson.plugins.git.Revision; @@ -31,6 +31,7 @@ import java.io.StringWriter; import java.lang.reflect.Field; import java.nio.file.Files; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -48,6 +49,7 @@ import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static java.util.stream.Collectors.toList; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -68,26 +70,23 @@ import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.RemoteConfig; import org.eclipse.jgit.transport.URIish; -import org.jvnet.hudson.test.Bug; +import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.TemporaryDirectoryAllocator; import org.objenesis.ObjenesisStd; -import com.google.common.base.Function; -import com.google.common.base.Predicate; import com.google.common.collect.Collections2; -import com.google.common.collect.Lists; /** * @author Nicolas De Loof */ public abstract class GitAPITestCase extends TestCase { - public final TemporaryDirectoryAllocator temporaryDirectoryAllocator = new TemporaryDirectoryAllocator(); + private final TemporaryDirectoryAllocator temporaryDirectoryAllocator = new TemporaryDirectoryAllocator(); protected hudson.EnvVars env = new hudson.EnvVars(); protected TaskListener listener; - protected LogHandler handler = null; + private LogHandler handler = null; private int logCount = 0; private static final String LOGGING_STARTED = "Logging started"; @@ -294,32 +293,32 @@ File touch(String path, String content) throws IOException { return f; } - public void rm(String path) { + void rm(String path) { file(path).delete(); } - public String contentOf(String path) throws IOException { + String contentOf(String path) throws IOException { return FileUtils.readFileToString(file(path), "UTF-8"); } /** * Creates a CGit implementation. Sometimes we need this for testing JGit impl. */ - protected CliGitAPIImpl cgit() throws Exception { + CliGitAPIImpl cgit() throws Exception { return (CliGitAPIImpl)Git.with(listener, env).in(repo).using("git").getClient(); } /** * Creates a JGit implementation. Sometimes we need this for testing CliGit impl. */ - protected JGitAPIImpl jgit() throws Exception { + JGitAPIImpl jgit() throws Exception { return (JGitAPIImpl)Git.with(listener, env).in(repo).using("jgit").getClient(); } /** * Creates a {@link Repository} object out of it. */ - protected FileRepository repo() throws IOException { + FileRepository repo() throws IOException { return bare ? new FileRepository(repo) : new FileRepository(new File(repo, ".git")); } @@ -333,14 +332,14 @@ ObjectId head() throws IOException, InterruptedException { /** * Casts the {@link #git} to {@link IGitAPI} */ - public IGitAPI igit() { + IGitAPI igit() { return (IGitAPI)git; } } protected WorkingArea w; - WorkingArea clone(String src) throws Exception { + protected WorkingArea clone(String src) throws Exception { WorkingArea x = new WorkingArea(); x.launchCommand("git", "clone", src, x.repoPath()); return new WorkingArea(x.repo); @@ -407,7 +406,7 @@ private ObjectId getMirrorHead() throws IOException, InterruptedException /** * Obtains the local mirror of https://github.com/jenkinsci/git-client-plugin.git and return URLish to it. */ - public String localMirror() throws IOException, InterruptedException { + protected String localMirror() throws IOException, InterruptedException { File base = new File(".").getAbsoluteFile(); for (File f=base; f!=null; f=f.getParentFile()) { if (new File(f,"target").exists()) { @@ -466,28 +465,63 @@ private void check_remote_url(final String repositoryName) throws InterruptedExc assertTrue("remote URL has not been updated", remotes.contains(localMirror())); } + private Collection getBranchNames(Collection branches) { + return branches.stream().map(Branch::getName).collect(toList()); + } + private void assertBranchesExist(Set branches, String ... names) throws InterruptedException { - Collection branchNames = Collections2.transform(branches, new Function() { - public String apply(Branch branch) { - return branch.getName(); - } - }); + Collection branchNames = getBranchNames(branches); for (String name : names) { - assertTrue(name + " branch not found in " + branchNames, branchNames.contains(name)); + assertThat(branchNames, hasItem(name)); } } private void assertBranchesNotExist(Set branches, String ... names) throws InterruptedException { - Collection branchNames = Collections2.transform(branches, new Function() { - public String apply(Branch branch) { - return branch.getName(); - } - }); + Collection branchNames = getBranchNames(branches); for (String name : names) { - assertFalse(name + " branch found in " + branchNames, branchNames.contains(name)); + assertThat(branchNames, not(hasItem(name))); } } + @NotImplementedInJGit + public void test_clone_default_timeout_logging() throws Exception { + w.git.clone_().url(localMirror()).repositoryName("origin").execute(); + + cloneTimeout = CliGitAPIImpl.TIMEOUT; + assertCloneTimeout(); + } + + @NotImplementedInJGit + public void test_fetch_default_timeout_logging() throws Exception { + w.git.clone_().url(localMirror()).repositoryName("origin").execute(); + + w.git.fetch_().from(new URIish("origin"), null).execute(); + + fetchTimeout = CliGitAPIImpl.TIMEOUT; + assertFetchTimeout(); + } + + @NotImplementedInJGit + public void test_checkout_default_timeout_logging() throws Exception { + w.git.clone_().url(localMirror()).repositoryName("origin").execute(); + + w.git.checkout().ref("origin/master").execute(); + + checkoutTimeout = CliGitAPIImpl.TIMEOUT; + assertCheckoutTimeout(); + } + + @NotImplementedInJGit + public void test_submodule_update_default_timeout_logging() throws Exception { + w.git.clone_().url(localMirror()).repositoryName("origin").execute(); + w.git.checkout().ref("origin/tests/getSubmodules").execute(); + + w.git.submoduleUpdate().execute(); + + submoduleUpdateTimeout = CliGitAPIImpl.TIMEOUT; + assertSubmoduleUpdateTimeout(); + } + public void test_setAuthor() throws Exception { final String authorName = "Test Author"; final String authorEmail = "jenkins@example.com"; @@ -567,12 +601,13 @@ public void test_clone_shallow() throws Exception assertBranchesExist(w.git.getBranches(), "master"); assertAlternatesFileNotFound(); /* JGit does not support shallow clone */ - assertEquals("isShallow?", w.igit() instanceof CliGitAPIImpl, w.cgit().isShallowRepository()); - final String shallow = ".git" + File.separator + "shallow"; - assertEquals("Shallow file existence: " + shallow, w.igit() instanceof CliGitAPIImpl, w.exists(shallow)); + boolean hasShallowCloneSupport = w.git instanceof CliGitAPIImpl && w.cgit().isAtLeastVersion(1, 5, 0, 0); + assertEquals("isShallow?", hasShallowCloneSupport, w.cgit().isShallowRepository()); + String shallow = ".git" + File.separator + "shallow"; + assertEquals("shallow file existence: " + shallow, hasShallowCloneSupport, w.exists(shallow)); } - public void test_clone_shallow_with_depth() throws IOException, InterruptedException + public void test_clone_shallow_with_depth() throws Exception { w.git.clone_().url(localMirror()).repositoryName("origin").shallow(true).depth(2).execute(); w.git.checkout("origin/master", "master"); @@ -580,8 +615,10 @@ public void test_clone_shallow_with_depth() throws IOException, InterruptedExcep assertBranchesExist(w.git.getBranches(), "master"); assertAlternatesFileNotFound(); /* JGit does not support shallow clone */ - final String shallow = ".git" + File.separator + "shallow"; - assertEquals("Shallow file existence: " + shallow, w.igit() instanceof CliGitAPIImpl, w.exists(shallow)); + boolean hasShallowCloneSupport = w.git instanceof CliGitAPIImpl && w.cgit().isAtLeastVersion(1, 5, 0, 0); + assertEquals("isShallow?", hasShallowCloneSupport, w.cgit().isShallowRepository()); + String shallow = ".git" + File.separator + "shallow"; + assertEquals("shallow file existence: " + shallow, hasShallowCloneSupport, w.exists(shallow)); } public void test_clone_shared() throws IOException, InterruptedException @@ -595,6 +632,16 @@ public void test_clone_shared() throws IOException, InterruptedException assertNoObjectsInRepository(); } + public void test_clone_null_branch() throws IOException, InterruptedException + { + w.git.clone_().url(localMirror()).repositoryName("origin").shared().execute(); + createRevParseBranch(); + w.git.checkout("origin/master", null); + check_remote_url("origin"); + assertAlternateFilePointsToLocalMirror(); + assertNoObjectsInRepository(); + } + public void test_clone_unshared() throws IOException, InterruptedException { w.git.clone_().url(localMirror()).repositoryName("origin").shared(false).execute(); @@ -672,34 +719,27 @@ public void test_clone_refspec() throws Exception { w.git.clone_().url(localMirror()).repositoryName("origin").execute(); final WorkingArea w2 = new WorkingArea(); w2.launchCommand("git", "clone", localMirror(), "./"); - w2.git.withRepository(new RepositoryCallback() { - public Void invoke(final Repository realRepo, VirtualChannel channel) throws IOException, InterruptedException { - return w.git.withRepository(new RepositoryCallback() { - public Void invoke(final Repository implRepo, VirtualChannel channel) { - final String realRefspec = realRepo.getConfig().getString(ConfigConstants.CONFIG_REMOTE_SECTION, Constants.DEFAULT_REMOTE_NAME, "fetch"); - final String implRefspec = implRepo.getConfig().getString(ConfigConstants.CONFIG_REMOTE_SECTION, Constants.DEFAULT_REMOTE_NAME, "fetch"); - assertEquals("Refspec not as git-clone", realRefspec, implRefspec); - return null; - } - }); - } - }); + w2.git.withRepository((final Repository realRepo, VirtualChannel channel) -> w.git.withRepository((final Repository implRepo, VirtualChannel channel1) -> { + final String realRefspec = realRepo.getConfig().getString(ConfigConstants.CONFIG_REMOTE_SECTION, Constants.DEFAULT_REMOTE_NAME, "fetch"); + final String implRefspec = implRepo.getConfig().getString(ConfigConstants.CONFIG_REMOTE_SECTION, Constants.DEFAULT_REMOTE_NAME, "fetch"); + assertEquals("Refspec not as git-clone", realRefspec, implRefspec); + return null; + })); } public void test_clone_refspecs() throws Exception { - List refspecs = Lists.newArrayList( + List refspecs = Arrays.asList( new RefSpec("+refs/heads/master:refs/remotes/origin/master"), new RefSpec("+refs/heads/1.4.x:refs/remotes/origin/1.4.x") ); w.git.clone_().url(localMirror()).refspecs(refspecs).repositoryName("origin").execute(); - w.git.withRepository(new RepositoryCallback() { - public Void invoke(Repository repo, VirtualChannel channel) throws IOException, InterruptedException { + w.git.withRepository((Repository repo, VirtualChannel channel) -> { String[] fetchRefSpecs = repo.getConfig().getStringList(ConfigConstants.CONFIG_REMOTE_SECTION, Constants.DEFAULT_REMOTE_NAME, "fetch"); assertEquals("Expected 2 refspecs", 2, fetchRefSpecs.length); assertEquals("Incorrect refspec 1", "+refs/heads/master:refs/remotes/origin/master", fetchRefSpecs[0]); assertEquals("Incorrect refspec 2", "+refs/heads/1.4.x:refs/remotes/origin/1.4.x", fetchRefSpecs[1]); return null; - }}); + }); Set remoteBranches = w.git.getRemoteBranches(); assertBranchesExist(remoteBranches, "origin/master"); assertBranchesExist(remoteBranches, "origin/1.4.x"); @@ -827,7 +867,36 @@ public void test_addRemoteUrl_local_clone() throws Exception { assertEquals("Wrong origin URL after add", localMirror(), w.git.getRemoteUrl("origin")); } - @Bug(20410) + public void test_clean_with_parameter() throws Exception { + w.init(); + w.commitEmpty("init"); + + String dirName1 = "dir1"; + String fileName1 = dirName1 + File.separator + "fileName1"; + String fileName2 = "fileName2"; + assertTrue("Did not create dir " + dirName1, w.file(dirName1).mkdir()); + w.touch(fileName1); + w.touch(fileName2); + + String dirName3 = "dir-with-submodule"; + File submodule = w.file(dirName3); + assertTrue("Did not create dir " + dirName3, submodule.mkdir()); + WorkingArea workingArea = new WorkingArea(submodule); + workingArea.init(); + workingArea.commitEmpty("init"); + + w.git.clean(false); + assertFalse(w.exists(dirName1)); + assertFalse(w.exists(fileName1)); + assertFalse(w.exists(fileName2)); + assertTrue(w.exists(dirName3)); + + w.git.clean(true); + assertFalse(w.exists(dirName3)); + + } + + @Issue({"JENKINS-20410", "JENKINS-27910", "JENKINS-22434"}) public void test_clean() throws Exception { w.init(); w.commitEmpty("init"); @@ -841,21 +910,27 @@ public void test_clean() throws Exception { */ String fileName = "\uD835\uDD65-\u5c4f\u5e55\u622a\u56fe-\u0041\u030a-\u00c5-\u212b-fileName.xml"; w.touch(fileName, "content " + fileName); - w.git.add(fileName); - w.git.commit(fileName); + withSystemLocaleReporting(fileName, () -> { + w.git.add(fileName); + w.git.commit(fileName); + }); /* JENKINS-27910 reported that certain cyrillic file names * failed to delete if the encoding was not UTF-8. */ String fileNameSwim = "\u00d0\u00bf\u00d0\u00bb\u00d0\u00b0\u00d0\u00b2\u00d0\u00b0\u00d0\u00bd\u00d0\u00b8\u00d0\u00b5-swim.png"; w.touch(fileNameSwim, "content " + fileNameSwim); - w.git.add(fileNameSwim); - w.git.commit(fileNameSwim); + withSystemLocaleReporting(fileNameSwim, () -> { + w.git.add(fileNameSwim); + w.git.commit(fileNameSwim); + }); String fileNameFace = "\u00d0\u00bb\u00d0\u00b8\u00d1\u2020\u00d0\u00be-face.png"; w.touch(fileNameFace, "content " + fileNameFace); - w.git.add(fileNameFace); - w.git.commit(fileNameFace); + withSystemLocaleReporting(fileNameFace, () -> { + w.git.add(fileNameFace); + w.git.commit(fileNameFace); + }); w.touch(".gitignore", ".test"); w.git.add(".gitignore"); @@ -907,6 +982,11 @@ public void test_clean() throws Exception { assertTrue("unexpected final status " + finalStatus + " dir contents: " + dirContents, finalStatus.contains("working directory clean") || finalStatus.contains("working tree clean")); } + private void assertExceptionMessageContains(GitException ge, String expectedSubstring) { + String actual = ge.getMessage().toLowerCase(); + assertTrue("Expected '" + expectedSubstring + "' exception message, but was: " + actual, actual.contains(expectedSubstring)); + } + public void test_fetch() throws Exception { /* Create a working repo containing a commit */ w.init(); @@ -920,7 +1000,7 @@ public void test_fetch() throws Exception { bare.init(true); w.git.setRemoteUrl("origin", bare.repoPath()); Set remoteBranchesEmpty = w.git.getRemoteBranches(); - assertEquals("Unexpected branch count", 0, remoteBranchesEmpty.size()); + assertThat(remoteBranchesEmpty, is(empty())); w.git.push("origin", "master"); ObjectId bareCommit1 = bare.git.getHeadRev(bare.repoPath(), "master"); assertEquals("bare != working", commit1, bareCommit1); @@ -931,7 +1011,7 @@ public void test_fetch() throws Exception { ObjectId newAreaHead = newArea.head(); assertEquals("bare != newArea", bareCommit1, newAreaHead); Set remoteBranches1 = newArea.git.getRemoteBranches(); - assertEquals("Unexpected branch count in " + remoteBranches1, 2, remoteBranches1.size()); + assertThat(getBranchNames(remoteBranches1), hasItems("origin/master")); assertEquals(bareCommit1, newArea.git.getHeadRev(newArea.repoPath(), "refs/heads/master")); /* Commit a new change to the original repo */ @@ -951,6 +1031,7 @@ public void test_fetch() throws Exception { RefSpec defaultRefSpec = new RefSpec("+refs/heads/*:refs/remotes/origin/*"); List refSpecs = new ArrayList<>(); refSpecs.add(defaultRefSpec); + newArea.cmd("git config fetch.prune false"); newArea.git.fetch(new URIish(bare.repo.toString()), refSpecs); /* Confirm the fetch did not alter working branch */ @@ -1020,18 +1101,18 @@ public void test_fetch() throws Exception { * command line less than 1.9. Assert that change arrives in * repo if git command line 1.9 or later. */ newArea.git.merge().setRevisionToMerge(bareCommit5).execute(); - assertTrue("JGit should not have copied the revision", newArea.git instanceof CliGitAPIImpl); - assertTrue("Wrong git version", w.cgit().isAtLeastVersion(1, 9, 0, 0)); + // JGit 4.9.0 and later copy the revision, JGit 4.8.0 and earlier did not + // assertTrue("JGit should not have copied the revision", newArea.git instanceof CliGitAPIImpl); + if (newArea.git instanceof CliGitAPIImpl) { + assertTrue("Wrong git version", w.cgit().isAtLeastVersion(1, 9, 0, 0)); + } expectedHead = bareCommit5; - } catch (org.eclipse.jgit.api.errors.JGitInternalException je) { - String expectedSubString = "Missing commit " + bareCommit5.name(); - assertTrue("Wrong jgit message :" + je.getMessage(), je.getMessage().contains(expectedSubString)); } catch (GitException ge) { assertTrue("Wrong cli git message :" + ge.getMessage(), ge.getMessage().contains("Could not merge") || ge.getMessage().contains("not something we can merge") || ge.getMessage().contains("does not point to a commit")); - assertTrue("Wrong message :" + ge.getMessage(), ge.getMessage().contains(bareCommit5.name())); + assertExceptionMessageContains(ge, bareCommit5.name()); } /* Assert that expected change is in repo after merge. With * git 1.7 and 1.8, it should be bareCommit4. With git 1.9 @@ -1043,7 +1124,7 @@ public void test_fetch() throws Exception { newArea.git.fetch("invalid-remote-name"); fail("Should have thrown an exception"); } catch (GitException ge) { - assertTrue("Wrong message :" + ge.getMessage(), ge.getMessage().contains("invalid-remote-name")); + assertExceptionMessageContains(ge, "invalid-remote-name"); } } @@ -1060,7 +1141,7 @@ public void test_push_tags() throws Exception { bare.init(true); w.git.setRemoteUrl("origin", bare.repoPath()); Set remoteBranchesEmpty = w.git.getRemoteBranches(); - assertEquals("Unexpected branch count", 0, remoteBranchesEmpty.size()); + assertThat(remoteBranchesEmpty, is(empty())); w.git.push("origin", "master"); ObjectId bareCommit1 = bare.git.getHeadRev(bare.repoPath(), "master"); assertEquals("bare != working", commit1, bareCommit1); @@ -1128,7 +1209,7 @@ public void test_push_tags() throws Exception { assertTrue("tag3 wasn't pushed", bare.cmd("git tag").contains("tag3")); } - @Bug(19591) + @Issue("JENKINS-19591") public void test_fetch_needs_preceding_prune() throws Exception { /* Create a working repo containing a commit */ w.init(); @@ -1136,8 +1217,8 @@ public void test_fetch_needs_preceding_prune() throws Exception { w.git.add("file1"); w.git.commit("commit1"); ObjectId commit1 = w.head(); - assertEquals("Wrong branch count", 1, w.git.getBranches().size()); - assertTrue("Remote branches should not exist", w.git.getRemoteBranches().isEmpty()); + assertThat(getBranchNames(w.git.getBranches()), contains("master")); + assertThat(w.git.getRemoteBranches(), is(empty())); /* Prune when a remote is not yet defined */ try { @@ -1156,8 +1237,8 @@ public void test_fetch_needs_preceding_prune() throws Exception { w.git.push("origin", "master"); ObjectId bareCommit1 = bare.git.getHeadRev(bare.repoPath(), "master"); assertEquals("bare != working", commit1, bareCommit1); - assertEquals("Wrong branch count", 1, w.git.getBranches().size()); - assertTrue("Remote branches should not exist", w.git.getRemoteBranches().isEmpty()); + assertThat(getBranchNames(w.git.getBranches()), contains("master")); + assertThat(w.git.getRemoteBranches(), is(empty())); /* Create a branch in working repo named "parent" */ w.git.branch("parent"); @@ -1166,23 +1247,22 @@ public void test_fetch_needs_preceding_prune() throws Exception { w.git.add("file2"); w.git.commit("commit2"); ObjectId commit2 = w.head(); - assertEquals("Wrong branch count", 2, w.git.getBranches().size()); - assertTrue("Remote branches should not exist", w.git.getRemoteBranches().isEmpty()); + assertThat(getBranchNames(w.git.getBranches()), containsInAnyOrder("master", "parent")); + assertThat(w.git.getRemoteBranches(), is(empty())); /* Push branch named "parent" to bare repo */ w.git.push("origin", "parent"); ObjectId bareCommit2 = bare.git.getHeadRev(bare.repoPath(), "parent"); assertEquals("working parent != bare parent", commit2, bareCommit2); - assertEquals("Wrong branch count", 2, w.git.getBranches().size()); - assertTrue("Remote branches should not exist", w.git.getRemoteBranches().isEmpty()); + assertThat(getBranchNames(w.git.getBranches()), containsInAnyOrder("master", "parent")); + assertThat(w.git.getRemoteBranches(), is(empty())); /* Clone new working repo from bare repo */ WorkingArea newArea = clone(bare.repoPath()); ObjectId newAreaHead = newArea.head(); assertEquals("bare != newArea", bareCommit1, newAreaHead); Set remoteBranches = newArea.git.getRemoteBranches(); - assertBranchesExist(remoteBranches, "origin/master", "origin/parent", "origin/HEAD"); - assertEquals("Wrong count in " + remoteBranches, 3, remoteBranches.size()); + assertThat(getBranchNames(remoteBranches), containsInAnyOrder("origin/master", "origin/parent", "origin/HEAD")); /* Checkout parent in new working repo */ newArea.git.checkout("origin/parent", "parent"); @@ -1192,7 +1272,7 @@ public void test_fetch_needs_preceding_prune() throws Exception { /* Delete parent branch from w */ w.git.checkout("master"); w.cmd("git branch -D parent"); - assertEquals("Wrong branch count", 1, w.git.getBranches().size()); + assertThat(getBranchNames(w.git.getBranches()), contains("master")); /* Delete parent branch on bare repo*/ bare.cmd("git branch -D parent"); @@ -1211,7 +1291,7 @@ public void test_fetch_needs_preceding_prune() throws Exception { ObjectId bareCommit3 = bare.git.getHeadRev(bare.repoPath(), "parent/a"); assertEquals("parent/a != bare", commit3, bareCommit3); remoteBranches = bare.git.getRemoteBranches(); - assertEquals("Wrong count in " + remoteBranches, 0, remoteBranches.size()); + assertThat(remoteBranches, is(empty())); RefSpec defaultRefSpec = new RefSpec("+refs/heads/*:refs/remotes/origin/*"); List refSpecs = new ArrayList<>(); @@ -1219,6 +1299,7 @@ public void test_fetch_needs_preceding_prune() throws Exception { try { /* Fetch parent/a into newArea repo - fails for * CliGitAPIImpl, succeeds for JGitAPIImpl */ + newArea.cmd("git config fetch.prune false"); newArea.git.fetch(new URIish(bare.repo.toString()), refSpecs); assertTrue("CliGit should have thrown an exception", newArea.git instanceof JGitAPIImpl); } catch (GitException ge) { @@ -1242,13 +1323,13 @@ public void test_fetch_timeout() throws Exception { } /** - * JGit 3.3.0 thru 3.6.0 "prune during fetch" prunes more remote - * branches than command line git prunes during fetch. This test - * should be used to evaluate future versions of JGit to see if - * pruning behavior more closely emulates command line git. - * - * This has been fixed using a workaround. + * JGit 3.3.0 thru 4.5.4 "prune during fetch" prunes more remote + * branches than command line git prunes during fetch. JGit 5.0.2 + * fixes the problem. + * Refer to https://bugs.eclipse.org/bugs/show_bug.cgi?id=533549 + * Refer to https://bugs.eclipse.org/bugs/show_bug.cgi?id=533806 */ + @Issue("JENKINS-26197") public void test_fetch_with_prune() throws Exception { WorkingArea bare = new WorkingArea(); bare.init(true); @@ -1257,12 +1338,11 @@ public void test_fetch_with_prune() throws Exception { /* master -> branch1 */ /* -> branch2 */ w.init(); + w.git.setRemoteUrl("origin", bare.repoPath()); w.touch("file-master", "file master content " + java.util.UUID.randomUUID().toString()); w.git.add("file-master"); w.git.commit("master-commit"); - ObjectId master = w.head(); assertEquals("Wrong branch count", 1, w.git.getBranches().size()); - w.git.setRemoteUrl("origin", bare.repoPath()); w.git.push("origin", "master"); /* master branch is now on bare repo */ w.git.checkout("master"); @@ -1270,8 +1350,7 @@ public void test_fetch_with_prune() throws Exception { w.touch("file-branch1", "file branch1 content " + java.util.UUID.randomUUID().toString()); w.git.add("file-branch1"); w.git.commit("branch1-commit"); - ObjectId branch1 = w.head(); - assertEquals("Wrong branch count", 2, w.git.getBranches().size()); + assertThat(getBranchNames(w.git.getBranches()), containsInAnyOrder("master", "branch1")); w.git.push("origin", "branch1"); /* branch1 is now on bare repo */ w.git.checkout("master"); @@ -1279,48 +1358,40 @@ public void test_fetch_with_prune() throws Exception { w.touch("file-branch2", "file branch2 content " + java.util.UUID.randomUUID().toString()); w.git.add("file-branch2"); w.git.commit("branch2-commit"); - ObjectId branch2 = w.head(); - assertEquals("Wrong branch count", 3, w.git.getBranches().size()); - assertTrue("Remote branches should not exist", w.git.getRemoteBranches().isEmpty()); + assertThat(getBranchNames(w.git.getBranches()), containsInAnyOrder("master", "branch1", "branch2")); + assertThat(w.git.getRemoteBranches(), is(empty())); w.git.push("origin", "branch2"); /* branch2 is now on bare repo */ /* Clone new working repo from bare repo */ WorkingArea newArea = clone(bare.repoPath()); ObjectId newAreaHead = newArea.head(); Set remoteBranches = newArea.git.getRemoteBranches(); - assertBranchesExist(remoteBranches, "origin/master", "origin/branch1", "origin/branch2", "origin/HEAD"); - assertEquals("Wrong count in " + remoteBranches, 4, remoteBranches.size()); + assertThat(getBranchNames(remoteBranches), containsInAnyOrder("origin/master", "origin/branch1", "origin/branch2", "origin/HEAD")); /* Remove branch1 from bare repo using original repo */ w.cmd("git push " + bare.repoPath() + " :branch1"); - RefSpec defaultRefSpec = new RefSpec("+refs/heads/*:refs/remotes/origin/*"); - List refSpecs = new ArrayList<>(); - refSpecs.add(defaultRefSpec); + List refSpecs = Arrays.asList(new RefSpec("+refs/heads/*:refs/remotes/origin/*")); /* Fetch without prune should leave branch1 in newArea */ + newArea.cmd("git config fetch.prune false"); newArea.git.fetch_().from(new URIish(bare.repo.toString()), refSpecs).execute(); remoteBranches = newArea.git.getRemoteBranches(); - assertBranchesExist(remoteBranches, "origin/master", "origin/branch1", "origin/branch2", "origin/HEAD"); - assertEquals("Wrong count in " + remoteBranches, 4, remoteBranches.size()); + assertThat(getBranchNames(remoteBranches), containsInAnyOrder("origin/master", "origin/branch1", "origin/branch2", "origin/HEAD")); /* Fetch with prune should remove branch1 from newArea */ newArea.git.fetch_().from(new URIish(bare.repo.toString()), refSpecs).prune().execute(); remoteBranches = newArea.git.getRemoteBranches(); - assertBranchesExist(remoteBranches, "origin/master", "origin/branch2", "origin/HEAD"); + assertThat(getBranchNames(remoteBranches), containsInAnyOrder("origin/master", "origin/branch2", "origin/HEAD")); - /* Git 1.7.1 on Red Hat 6 does not prune branch1, don't fail the test + /* Git older than 1.7.9 (like 1.7.1 on Red Hat 6) does not prune branch1, don't fail the test * on that old git version. */ - int expectedBranchCount = 3; if (newArea.git instanceof CliGitAPIImpl && !w.cgit().isAtLeastVersion(1, 7, 9, 0)) { - expectedBranchCount = 4; - assertBranchesExist(remoteBranches, "origin/master", "origin/branch1", "origin/branch2", "origin/HEAD"); + assertThat(getBranchNames(remoteBranches), containsInAnyOrder("origin/master", "origin/branch1", "origin/branch2", "origin/HEAD")); } else { - assertBranchesExist(remoteBranches, "origin/master", "origin/branch2", "origin/HEAD"); - assertBranchesNotExist(remoteBranches, "origin/branch1"); + assertThat(getBranchNames(remoteBranches), containsInAnyOrder("origin/master", "origin/branch2", "origin/HEAD")); } - assertEquals("Wrong remote branch count", expectedBranchCount, remoteBranches.size()); } public void test_fetch_from_url() throws Exception { @@ -1366,9 +1437,11 @@ public void test_fetch_shallow() throws Exception { assertBranchesExist(w.git.getRemoteBranches(), "origin/master"); final String alternates = ".git" + File.separator + "objects" + File.separator + "info" + File.separator + "alternates"; assertFalse("Alternates file found: " + alternates, w.exists(alternates)); - /* JGit does not support shallow clone */ - final String shallow = ".git" + File.separator + "shallow"; - assertEquals("Shallow file: " + shallow, w.igit() instanceof CliGitAPIImpl, w.exists(shallow)); + /* JGit does not support shallow fetch */ + boolean hasShallowFetchSupport = w.git instanceof CliGitAPIImpl && w.cgit().isAtLeastVersion(1, 5, 0, 0); + assertEquals("isShallow?", hasShallowFetchSupport, w.cgit().isShallowRepository()); + String shallow = ".git" + File.separator + "shallow"; + assertEquals("shallow file existence: " + shallow, hasShallowFetchSupport, w.exists(shallow)); } public void test_fetch_shallow_depth() throws Exception { @@ -1379,9 +1452,11 @@ public void test_fetch_shallow_depth() throws Exception { assertBranchesExist(w.git.getRemoteBranches(), "origin/master"); final String alternates = ".git" + File.separator + "objects" + File.separator + "info" + File.separator + "alternates"; assertFalse("Alternates file found: " + alternates, w.exists(alternates)); - /* JGit does not support shallow clone */ - final String shallow = ".git" + File.separator + "shallow"; - assertEquals("Shallow file: " + shallow, w.igit() instanceof CliGitAPIImpl, w.exists(shallow)); + /* JGit does not support shallow fetch */ + boolean hasShallowFetchSupport = w.git instanceof CliGitAPIImpl && w.cgit().isAtLeastVersion(1, 5, 0, 0); + assertEquals("isShallow?", hasShallowFetchSupport, w.cgit().isShallowRepository()); + String shallow = ".git" + File.separator + "shallow"; + assertEquals("shallow file existence: " + shallow, hasShallowFetchSupport, w.exists(shallow)); } public void test_fetch_noTags() throws Exception { @@ -1394,7 +1469,7 @@ public void test_fetch_noTags() throws Exception { assertTrue("Tags have been found : " + tags, tags.isEmpty()); } - @Bug(37794) + @Issue("JENKINS-37794") public void test_getTagNames_supports_slashes_in_tag_names() throws Exception { w.init(); w.commitEmpty("init-getTagNames-supports-slashes"); @@ -1440,7 +1515,7 @@ public void test_create_branch() throws Exception { assertTrue("test branch not listed", branches.contains("test")); } - @Bug(34309) + @Issue("JENKINS-34309") public void test_list_branches() throws Exception { w.init(); Set branches = w.git.getBranches(); @@ -1553,43 +1628,51 @@ public void test_delete_branch() throws Exception { } } - @Bug(23299) + @Issue("JENKINS-23299") public void test_create_tag() throws Exception { w.init(); String gitDir = w.repoPath() + File.separator + ".git"; w.commitEmpty("init"); - ObjectId init = w.git.revParse("HEAD"); // Remember SHA1 of init commit - w.git.tag("test", "this is a tag"); + ObjectId commitId = w.git.revParse("HEAD"); + w.git.tag("test", "this is an annotated tag"); - /* JGit seems to have the better behavior in this case, always + /* + * Spec: "test" (short tag syntax) + * CliGit does not support this syntax for remotes. + * JGit fully supports this syntax. + * + * JGit seems to have the better behavior in this case, always * returning the SHA1 of the commit. Most users are using * command line git, so the difference is retained in command * line git for compatibility with any legacy command line git - * use cases which depend on returning the SHA-1 of the - * annotated tag rather than the SHA-1 of the commit to which - * the annotated tag points. + * use cases which depend on returning null rather than the + * SHA-1 of the commit to which the annotated tag points. */ - ObjectId testTag = w.git.getHeadRev(gitDir, "test"); // Remember SHA1 of annotated test tag + String shortTagRef = "test"; + ObjectId tagHeadIdByShortRef = w.git.getHeadRev(gitDir, shortTagRef); if (w.git instanceof JGitAPIImpl) { - assertEquals("Annotated tag does not match SHA1", init, testTag); + assertEquals("annotated tag does not match commit SHA1", commitId, tagHeadIdByShortRef); } else { - assertNotEquals("Annotated tag unexpectedly equals SHA1", init, testTag); + assertNull("annotated tag unexpectedly not null", tagHeadIdByShortRef); } + assertEquals("annotated tag does not match commit SHA1", commitId, w.git.revParse(shortTagRef)); - /* Because refs/tags/test syntax is more specific than "test", - * and because the more specific syntax was only introduced in - * more recent git client plugin versions (like 1.10.0 and - * later), the CliGit and JGit behavior are kept the same here - * in order to fix JENKINS-23299. + /* + * Spec: "refs/tags/test" (more specific tag syntax) + * CliGit and JGit fully support this syntax. */ - ObjectId testTagCommit = w.git.getHeadRev(gitDir, "refs/tags/test"); // SHA1 of commit identified by test tag - assertEquals("Annotated tag doesn't match queried commit SHA1", init, testTagCommit); - assertEquals(init, w.git.revParse("test")); // SHA1 of commit identified by test tag - assertEquals(init, w.git.revParse("refs/tags/test")); // SHA1 of commit identified by test tag - assertTrue("test tag not created", w.cmd("git tag").contains("test")); - String message = w.cmd("git tag -l -n1"); - assertTrue("unexpected test tag message : " + message, message.contains("this is a tag")); - assertNull(w.git.getHeadRev(gitDir, "not-a-valid-tag")); // Confirm invalid tag returns null + String longTagRef = "refs/tags/test"; + assertEquals("annotated tag does not match commit SHA1", commitId, w.git.getHeadRev(gitDir, longTagRef)); + assertEquals("annotated tag does not match commit SHA1", commitId, w.git.revParse(longTagRef)); + + String tagNames = w.cmd("git tag -l").trim(); + assertEquals("tag not created", "test", tagNames); + + String tagNamesWithMessages = w.cmd("git tag -l -n1"); + assertTrue("unexpected tag message : " + tagNamesWithMessages, tagNamesWithMessages.contains("this is an annotated tag")); + + ObjectId invalidTagId = w.git.getHeadRev(gitDir, "not-a-valid-tag"); + assertNull("did not expect reference for invalid tag but got : " + invalidTagId, invalidTagId); } public void test_delete_tag() throws Exception { @@ -1849,7 +1932,7 @@ public void test_push_from_shallow_clone() throws Exception { assertEquals(sha1.name(), remoteSha1); } catch (GitException e) { // expected for git cli < 1.9.0 - assertTrue("Wrong exception message: " + e, e.getMessage().contains("push from shallow repository")); + assertExceptionMessageContains(e, "push from shallow repository"); assertFalse("git >= 1.9.0 can't push from shallow repository", w.cgit().isAtLeastVersion(1, 9, 0, 0)); } } @@ -1887,7 +1970,7 @@ public void test_notes_append_first_note() throws Exception { /** * A rev-parse warning message should not break revision parsing. */ - @Bug(11177) + @Issue("JENKINS-11177") public void test_jenkins_11177() throws Exception { w.init(); @@ -1996,17 +2079,7 @@ public void test_submodule_checkout_and_clean_transitions() throws Exception { assertDirExists(modulesDir); assertFileExists(keeperFile); assertFileContents(keeperFile, ""); - /* Command line git checkout creates empty directories for modules, JGit does not */ - /* That behavioral difference seems harmless */ - if (w.git instanceof CliGitAPIImpl) { - assertSubmoduleDirs(w.repo, true, false); - } else { - assertDirNotFound(ntpDir); - assertDirNotFound(firewallDir); - assertDirNotFound(sshkeysDir); - assertFileNotFound(ntpContributingFile); - assertFileNotFound(sshkeysModuleFile); - } + assertSubmoduleDirs(w.repo, true, false); /* Call submodule update without recursion */ w.git.submoduleUpdate().recursive(false).execute(); @@ -2019,9 +2092,8 @@ public void test_submodule_checkout_and_clean_transitions() throws Exception { assertSubmoduleRepository(new File(w.repo, "modules/firewall")); assertSubmoduleRepository(new File(w.repo, "modules/sshkeys")); } else { - assertDirNotFound(ntpDir); - assertDirNotFound(firewallDir); - assertDirNotFound(sshkeysDir); + /* JGit does not fully support renamed submodules - creates directories but not content */ + assertSubmoduleDirs(w.repo, true, false); } /* Call submodule update with recursion */ @@ -2035,9 +2107,8 @@ public void test_submodule_checkout_and_clean_transitions() throws Exception { assertSubmoduleRepository(new File(w.repo, "modules/firewall")); assertSubmoduleRepository(new File(w.repo, "modules/sshkeys")); } else { - assertDirNotFound(ntpDir); - assertDirNotFound(firewallDir); - assertDirNotFound(sshkeysDir); + /* JGit does not fully support renamed submodules - creates directories but not content */ + assertSubmoduleDirs(w.repo, true, false); } String notSubBranchName = "tests/notSubmodules"; @@ -2137,14 +2208,7 @@ public void test_submodule_checkout_and_clean_transitions() throws Exception { // w.git.checkout().ref(subRefName).branch(subBranch).execute(); w.git.checkout().ref(subRefName).execute(); assertDirExists(modulesDir); - if (w.git instanceof CliGitAPIImpl) { - assertSubmoduleDirs(w.repo, true, false); - } else { - /* JGit does not support renamed submodules - creates none of the directories */ - assertDirNotFound(ntpDir); - assertDirNotFound(firewallDir); - assertDirNotFound(sshkeysDir); - } + assertSubmoduleDirs(w.repo, true, false); w.git.submoduleClean(true); assertSubmoduleDirs(w.repo, true, false); @@ -2227,12 +2291,11 @@ private void assertSubmoduleRepository(File submoduleDir) throws Exception { /* Assert that when we invoke the repository callback it gets a * functioning repository object */ - submoduleClient.withRepository(new RepositoryCallback() { - public Void invoke(final Repository repo, VirtualChannel channel) throws IOException, InterruptedException { - assertTrue(repo.getDirectory() + " is not a valid repository", - repo.getObjectDatabase().exists()); - return null; - }}); + submoduleClient.withRepository((final Repository repo, VirtualChannel channel) -> { + assertTrue(repo.getDirectory() + " is not a valid repository", + repo.getObjectDatabase().exists()); + return null; + }); } private String listDir(File dir) { @@ -2357,7 +2420,7 @@ public void assertFixSubmoduleUrlsThrows() throws InterruptedException { } catch (GitException ge) { assertTrue("GitException not on CliGit", w.igit() instanceof CliGitAPIImpl); assertTrue("Wrong message in " + ge.getMessage(), ge.getMessage().startsWith("Could not determine remote")); - assertTrue("Wrong remote in " + ge.getMessage(), ge.getMessage().contains("origin")); + assertExceptionMessageContains(ge, "origin"); } } @@ -2466,12 +2529,12 @@ private void base_checkout_replaces_tracked_changes(boolean defineBranch) throws assertTrue("Missing untracked file", w.file("untracked-file").exists()); } - @Bug(23424) + @Issue("JENKINS-23424") public void test_checkout_replaces_tracked_changes() throws Exception { base_checkout_replaces_tracked_changes(false); } - @Bug(23424) + @Issue("JENKINS-23424") public void test_checkout_replaces_tracked_changes_with_branch() throws Exception { base_checkout_replaces_tracked_changes(true); } @@ -2486,7 +2549,7 @@ public void test_checkout_replaces_tracked_changes_with_branch() throws Exceptio * checkout, after the submodule branch checkout, and within one * of the submodules. */ - @Bug(8122) + @Issue("JENKINS-8122") public void test_submodule_tags_not_fetched_into_parent() throws Exception { w.git.clone_().url(localMirror()).repositoryName("origin").execute(); checkoutTimeout = 1 + random.nextInt(60 * 24); @@ -2563,6 +2626,60 @@ public void test_submodule_update() throws Exception { assertTrue("modules/sshkeys does not exist", w.exists("modules/sshkeys")); } assertFixSubmoduleUrlsThrows(); + + String shallow = Paths.get(".git", "modules", "module", "1", "shallow").toString(); + assertFalse("shallow file existence: " + shallow, w.exists(shallow)); + } + + public void test_submodule_update_shallow() throws Exception { + WorkingArea remote = setupRepositoryWithSubmodule(); + w.git.clone_().url("file://" + remote.file("dir-repository").getAbsolutePath()).repositoryName("origin").execute(); + w.git.checkout().branch("master").ref("origin/master").execute(); + w.git.submoduleInit(); + w.git.submoduleUpdate().shallow(true).execute(); + + boolean hasShallowSubmoduleSupport = w.git instanceof CliGitAPIImpl && w.cgit().isAtLeastVersion(1, 8, 4, 0); + + String shallow = Paths.get(".git", "modules", "submodule", "shallow").toString(); + assertEquals("shallow file existence: " + shallow, hasShallowSubmoduleSupport, w.exists(shallow)); + + int localSubmoduleCommits = w.cgit().subGit("submodule").revList("master").size(); + int remoteSubmoduleCommits = remote.cgit().subGit("dir-submodule").revList("master").size(); + assertEquals("submodule commit count didn't match", hasShallowSubmoduleSupport ? 1 : remoteSubmoduleCommits, localSubmoduleCommits); + } + + public void test_submodule_update_shallow_with_depth() throws Exception { + WorkingArea remote = setupRepositoryWithSubmodule(); + w.git.clone_().url("file://" + remote.file("dir-repository").getAbsolutePath()).repositoryName("origin").execute(); + w.git.checkout().branch("master").ref("origin/master").execute(); + w.git.submoduleInit(); + w.git.submoduleUpdate().shallow(true).depth(2).execute(); + + boolean hasShallowSubmoduleSupport = w.git instanceof CliGitAPIImpl && w.cgit().isAtLeastVersion(1, 8, 4, 0); + + String shallow = Paths.get(".git", "modules", "submodule", "shallow").toString(); + assertEquals("shallow file existence: " + shallow, hasShallowSubmoduleSupport, w.exists(shallow)); + + int localSubmoduleCommits = w.cgit().subGit("submodule").revList("master").size(); + int remoteSubmoduleCommits = remote.cgit().subGit("dir-submodule").revList("master").size(); + assertEquals("submodule commit count didn't match", hasShallowSubmoduleSupport ? 2 : remoteSubmoduleCommits, localSubmoduleCommits); + } + + @NotImplementedInJGit + public void test_submodule_update_with_threads() throws Exception { + w.init(); + w.git.clone_().url(localMirror()).repositoryName("sub2_origin").execute(); + w.git.checkout().branch("tests/getSubmodules").ref("sub2_origin/tests/getSubmodules").deleteBranchIfExist(true).execute(); + w.git.submoduleInit(); + w.git.submoduleUpdate().threads(3).execute(); + + assertTrue("modules/firewall does not exist", w.exists("modules/firewall")); + assertTrue("modules/ntp does not exist", w.exists("modules/ntp")); + // JGit submodule implementation doesn't handle renamed submodules + if (w.igit() instanceof CliGitAPIImpl) { + assertTrue("modules/sshkeys does not exist", w.exists("modules/sshkeys")); + } + assertFixSubmoduleUrlsThrows(); } @NotImplementedInJGit @@ -2655,17 +2772,17 @@ public void test_sparse_checkout() throws Exception { workingArea.git.clone_().url(w.repoPath()).execute(); checkoutTimeout = 1 + random.nextInt(60 * 24); - workingArea.git.checkout().ref("origin/master").branch("master").deleteBranchIfExist(true).sparseCheckoutPaths(Lists.newArrayList("dir1")).timeout(checkoutTimeout).execute(); + workingArea.git.checkout().ref("origin/master").branch("master").deleteBranchIfExist(true).sparseCheckoutPaths(Arrays.asList("dir1")).timeout(checkoutTimeout).execute(); assertTrue(workingArea.exists("dir1")); assertFalse(workingArea.exists("dir2")); assertFalse(workingArea.exists("dir3")); - workingArea.git.checkout().ref("origin/master").branch("master").deleteBranchIfExist(true).sparseCheckoutPaths(Lists.newArrayList("dir2")).timeout(checkoutTimeout).execute(); + workingArea.git.checkout().ref("origin/master").branch("master").deleteBranchIfExist(true).sparseCheckoutPaths(Arrays.asList("dir2")).timeout(checkoutTimeout).execute(); assertFalse(workingArea.exists("dir1")); assertTrue(workingArea.exists("dir2")); assertFalse(workingArea.exists("dir3")); - workingArea.git.checkout().ref("origin/master").branch("master").deleteBranchIfExist(true).sparseCheckoutPaths(Lists.newArrayList("dir1", "dir2")).timeout(checkoutTimeout).execute(); + workingArea.git.checkout().ref("origin/master").branch("master").deleteBranchIfExist(true).sparseCheckoutPaths(Arrays.asList("dir1", "dir2")).timeout(checkoutTimeout).execute(); assertTrue(workingArea.exists("dir1")); assertTrue(workingArea.exists("dir2")); assertFalse(workingArea.exists("dir3")); @@ -2710,20 +2827,14 @@ public void test_hasSubmodules() throws Exception { assertFixSubmoduleUrlsThrows(); } - private boolean isJava6() { - if (System.getProperty("java.version").startsWith("1.6")) { - return true; - } - return false; - } - - /** - * core.symlinks is set to false by msysgit on Windows and by JGit - * 3.3.0 on all platforms. It is not set on Linux. Refer to - * JENKINS-21168, JENKINS-22376, and JENKINS-22391 for details. + /* + * core.symlinks is set to false by git for WIndows. + * It is not set on Linux. + * See also JENKINS-22376 and JENKINS-22391 */ + @Issue("JENKINS-21168") private void checkSymlinkSetting(WorkingArea area) throws IOException { - String expected = SystemUtils.IS_OS_WINDOWS || (area.git instanceof JGitAPIImpl && isJava6()) ? "false" : ""; + String expected = SystemUtils.IS_OS_WINDOWS ? "false" : ""; String symlinkValue = null; try { symlinkValue = w.cmd(true, "git config core.symlinks").trim(); @@ -2931,7 +3042,7 @@ public void test_merge_strategy_correct_fail() throws Exception { } } - @Bug(12402) + @Issue("JENKINS-12402") public void test_merge_fast_forward_mode_ff() throws Exception { w.init(); @@ -3164,6 +3275,30 @@ public void test_merge_with_message() throws Exception { assertEquals("Custom message merge failed. Should have set custom merge message.", mergeMessage, resultMessage); } + public void test_changelog_with_merge_commit() throws Exception { + w.init(); + w.commitEmpty("init"); + + // First commit to branch1 + w.git.branch("branch1"); + w.git.checkout("branch1"); + w.touch("file1", "content1"); + w.git.add("file1"); + w.git.commit("commit1"); + String commitSha1 = w.git.revParse("HEAD").name(); + + // Merge branch1 into master + w.git.checkout("master"); + String mergeMessage = "Merge message to be tested."; + w.git.merge().setMessage(mergeMessage).setGitPluginFastForwardMode(MergeCommand.GitPluginFastForwardMode.NO_FF).setRevisionToMerge(w.git.getHeadRev(w.repoPath(), "branch1")).execute(); + // Obtain last commit message + String mergeSha1 = w.git.revParse("HEAD").name(); + + assertThat("Merge commit exists in output",get_changelog(w.git.changelog()),not(containsString(mergeSha1))); + assertThat("Merge commit exists in output",get_changelog(w.git.changelog().listMerges(false)),not(containsString(mergeSha1))); + assertThat("Merge commit is not the first commit",get_changelog(w.git.changelog().listMerges(true)),startsWith(("commit " + mergeSha1))); + } + @Deprecated public void test_merge_refspec() throws Exception { w.init(); @@ -3198,14 +3333,14 @@ public void test_merge_refspec() throws Exception { assertTrue("Exception not thrown by CliGit", w.git instanceof CliGitAPIImpl); } catch (GitException moa) { assertFalse("Exception thrown by CliGit", w.git instanceof CliGitAPIImpl); - assertTrue("Exception message didn't mention " + badBase.toString(), moa.getMessage().contains(badSHA1)); + assertExceptionMessageContains(moa, badSHA1); } try { assertNull("Base unexpected for bad SHA1", w.igit().mergeBase(badBase, branch1)); assertTrue("Exception not thrown by CliGit", w.git instanceof CliGitAPIImpl); } catch (GitException moa) { assertFalse("Exception thrown by CliGit", w.git instanceof CliGitAPIImpl); - assertTrue("Exception message didn't mention " + badBase.toString(), moa.getMessage().contains(badSHA1)); + assertExceptionMessageContains(moa, badSHA1); } w.igit().merge("branch1"); @@ -3328,7 +3463,7 @@ public void test_changelog_abort() throws InterruptedException, IOException assertTrue("No SHA1 in " + writer.toString(), writer.toString().contains(sha1)); } - @Bug(23299) + @Issue("JENKINS-23299") public void test_getHeadRev() throws Exception { Map heads = w.git.getHeadRev(remoteMirrorURL); ObjectId master = w.git.getHeadRev(remoteMirrorURL, "refs/heads/master"); @@ -3359,7 +3494,7 @@ public void test_getHeadRevFromPublicRepoWithInvalidCredential() throws Exceptio assertEquals("URL is " + remoteMirrorURL + ", heads is " + heads, master, heads.get("refs/heads/master")); } - @Bug(25444) + @Issue("JENKINS-25444") public void test_fetch_delete_cleans() throws Exception { w.init(); w.touch("file1", "old"); @@ -3414,15 +3549,15 @@ public void test_getHeadRev_namespaces_withSimpleBranchNames() throws Exception w = clone(tempRemoteDir.getAbsolutePath()); final String remote = tempRemoteDir.getAbsolutePath(); - final String[][] checkBranchSpecs = {}; -//TODO: Fix and enable test -// { -// {"master", commits.getProperty("refs/heads/master")}, -// {"a_tests/b_namespace1/master", commits.getProperty("refs/heads/a_tests/b_namespace1/master")}, -// {"a_tests/b_namespace2/master", commits.getProperty("refs/heads/a_tests/b_namespace2/master")}, -// {"a_tests/b_namespace3/master", commits.getProperty("refs/heads/a_tests/b_namespace3/master")}, -// {"b_namespace3/master", commits.getProperty("refs/heads/b_namespace3/master")} -// }; + final String[][] checkBranchSpecs = + //TODO: Fix and enable test + { + {"a_tests/b_namespace1/master", commits.getProperty("refs/heads/a_tests/b_namespace1/master")}, + // {"a_tests/b_namespace2/master", commits.getProperty("refs/heads/a_tests/b_namespace2/master")}, + // {"a_tests/b_namespace3/master", commits.getProperty("refs/heads/a_tests/b_namespace3/master")}, + // {"b_namespace3/master", commits.getProperty("refs/heads/b_namespace3/master")}, + // {"master", commits.getProperty("refs/heads/master")}, + }; for(String[] branch : checkBranchSpecs) { final ObjectId objectId = ObjectId.fromString(branch[1]); @@ -3546,7 +3681,7 @@ public void test_getRemoteReferences_withMatchingPattern() throws Exception { try { references = w.git.getRemoteReferences(remoteMirrorURL, "notexists-*", false, false); } catch (GitException ge) { - assertTrue("Wrong exception message: " + ge, ge.getMessage().contains("unexpected ls-remote output")); + assertExceptionMessageContains(ge, "unexpected ls-remote output"); } assertTrue(references.isEmpty()); } @@ -3714,15 +3849,10 @@ public void test_getHeadRev_returns_accurate_SHA1_values() throws Exception { check_headRev(w.repoPath(), getMirrorHead()); } - private void check_changelog_sha1(final String sha1, final String branchName) throws InterruptedException - { - ChangelogCommand changelogCommand = w.git.changelog(); - changelogCommand.max(1); + private String get_changelog(ChangelogCommand changelogCommand) throws InterruptedException{ StringWriter writer = new StringWriter(); - changelogCommand.to(writer); - changelogCommand.execute(); - String splitLog[] = writer.toString().split("[\\n\\r]", 3); // Extract first line of changelog - assertEquals("Wrong changelog line 1 on branch " + branchName, "commit " + sha1, splitLog[0]); + changelogCommand.to(writer).execute(); + return writer.toString(); } public void test_changelog() throws Exception { @@ -3732,7 +3862,7 @@ public void test_changelog() throws Exception { w.git.add("changelog-file"); w.git.commit("changelog-commit-message"); String sha1 = w.git.revParse("HEAD").name(); - check_changelog_sha1(sha1, "master"); + assertThat("Wrong changelog line 1 on branch master", get_changelog(w.git.changelog().max(1)),startsWith("commit " + sha1)); } public void test_show_revision_for_merge() throws Exception { @@ -3742,27 +3872,16 @@ public void test_show_revision_for_merge() throws Exception { List revisionDetails = w.git.showRevision(from, to); - Collection commits = Collections2.filter(revisionDetails, new Predicate() { - public boolean apply(String detail) { - return detail.startsWith("commit "); - } - }); + Collection commits = Collections2.filter(revisionDetails, (String detail) -> detail.startsWith("commit ")); assertEquals(3, commits.size()); assertTrue(commits.contains("commit 4f2964e476776cf59be3e033310f9177bedbf6a8")); // Merge commit is duplicated as have to capture changes that may have been made as part of merge assertTrue(commits.contains("commit b53374617e85537ec46f86911b5efe3e4e2fa54b (from 4f2964e476776cf59be3e033310f9177bedbf6a8)")); assertTrue(commits.contains("commit b53374617e85537ec46f86911b5efe3e4e2fa54b (from 45e76942914664ee19f31d90e6f2edbfe0d13a46)")); - Collection diffs = Collections2.filter(revisionDetails, new Predicate() { - public boolean apply(String detail) { - return detail.startsWith(":"); - } - }); - Collection paths = Collections2.transform(diffs, new Function() { - public String apply(String diff) { - return diff.substring(diff.indexOf('\t')+1).trim(); // Windows diff output ^M removed by trim() - } - }); + Collection diffs = Collections2.filter(revisionDetails, (String detail) -> detail.startsWith(":")); + Collection paths = Collections2.transform(diffs, (String diff) -> diff.substring(diff.indexOf('\t')+1).trim() // Windows diff output ^M removed by trim() + ); assertTrue(paths.contains(".gitignore")); // Some irrelevant changes will be listed due to merge commit @@ -3786,20 +3905,12 @@ public void test_show_revision_for_merge_exclude_files() throws Exception { List revisionDetails = w.git.showRevision(from, to, useRawOutput); - Collection commits = Collections2.filter(revisionDetails, new Predicate() { - public boolean apply(String detail) { - return detail.startsWith("commit "); - } - }); + Collection commits = Collections2.filter(revisionDetails, (String detail) -> detail.startsWith("commit ")); assertEquals(2, commits.size()); assertTrue(commits.contains("commit 4f2964e476776cf59be3e033310f9177bedbf6a8")); assertTrue(commits.contains("commit b53374617e85537ec46f86911b5efe3e4e2fa54b")); - Collection diffs = Collections2.filter(revisionDetails, new Predicate() { - public boolean apply(String detail) { - return detail.startsWith(":"); - } - }); + Collection diffs = Collections2.filter(revisionDetails, (String detail) -> detail.startsWith(":")); assertTrue(diffs.isEmpty()); } @@ -3827,16 +3938,12 @@ public void test_show_revision_for_single_commit() throws Exception { w = clone(localMirror()); ObjectId to = ObjectId.fromString("51de9eda47ca8dcf03b2af58dfff7355585f0d0c"); List revisionDetails = w.git.showRevision(null, to); - Collection commits = Collections2.filter(revisionDetails, new Predicate() { - public boolean apply(String detail) { - return detail.startsWith("commit "); - } - }); + Collection commits = Collections2.filter(revisionDetails, (String detail) -> detail.startsWith("commit ")); assertEquals(1, commits.size()); assertTrue(commits.contains("commit 51de9eda47ca8dcf03b2af58dfff7355585f0d0c")); } - @Bug(22343) + @Issue("JENKINS-22343") public void test_show_revision_for_first_commit() throws Exception { w.init(); w.touch("a"); @@ -3844,11 +3951,7 @@ public void test_show_revision_for_first_commit() throws Exception { w.git.commit("first"); ObjectId first = w.head(); List revisionDetails = w.git.showRevision(first); - Collection commits = Collections2.filter(revisionDetails, new Predicate() { - public boolean apply(String detail) { - return detail.startsWith("commit "); - } - }); + Collection commits = Collections2.filter(revisionDetails, (String detail) -> detail.startsWith("commit ")); assertTrue("Commits '" + commits + "' missing " + first.getName(), commits.contains("commit " + first.getName())); assertEquals("Commits '" + commits + "' wrong size", 1, commits.size()); } @@ -3990,7 +4093,7 @@ public void test_checkout() throws Exception { assertEquals("Wrong SHA1 as checkout of git-client-1.6.0", sha1Expected, sha1); } - @Bug(37185) + @Issue("JENKINS-37185") @NotImplementedInJGit /* JGit doesn't have timeout */ public void test_checkout_honor_timeout() throws Exception { w = clone(localMirror()); @@ -3999,7 +4102,7 @@ public void test_checkout_honor_timeout() throws Exception { w.git.checkout().branch("master").ref("origin/master").timeout(checkoutTimeout).deleteBranchIfExist(true).execute(); } - @Bug(25353) + @Issue("JENKINS-25353") @NotImplementedInJGit /* JGit lock file management ignored for now */ public void test_checkout_interrupted() throws Exception { w = clone(localMirror()); @@ -4018,7 +4121,7 @@ public void test_checkout_interrupted() throws Exception { assertFalse("lock file '" + lockFile.getCanonicalPath() + " not removed by cleanup", lockFile.exists()); } - @Bug(25353) + @Issue("JENKINS-25353") @NotImplementedInJGit /* JGit lock file management ignored for now */ public void test_checkout_interrupted_with_existing_lock() throws Exception { w = clone(localMirror()); @@ -4039,7 +4142,7 @@ public void test_checkout_interrupted_with_existing_lock() throws Exception { assertTrue("lock file '" + lockFile.getCanonicalPath() + " removed by cleanup", lockFile.exists()); } - @Bug(19108) + @Issue("JENKINS-19108") public void test_checkoutBranch() throws Exception { w.init(); w.commitEmpty("c1"); @@ -4051,7 +4154,7 @@ public void test_checkoutBranch() throws Exception { assertEquals(w.head(),w.git.revParse("t1")); assertEquals(w.head(),w.git.revParse("foo")); - Ref head = w.repo().getRef("HEAD"); + Ref head = w.repo().exactRef("HEAD"); assertTrue(head.isSymbolic()); assertEquals("refs/heads/foo",head.getTarget().getName()); } @@ -4113,20 +4216,20 @@ public void test_revList_remote_branch() throws Exception { w = clone(localMirror()); List revList = w.git.revList("origin/1.4.x"); assertEquals("Wrong list size: " + revList, 267, revList.size()); - Ref branchRef = w.repo().getRef("origin/1.4.x"); + Ref branchRef = w.repo().findRef("origin/1.4.x"); assertTrue("origin/1.4.x not in revList", revList.contains(branchRef.getObjectId())); } public void test_revList_tag() throws Exception { w.init(); w.commitEmpty("c1"); - Ref commitRefC1 = w.repo().getRef("HEAD"); + Ref commitRefC1 = w.repo().exactRef("HEAD"); w.tag("t1"); - Ref tagRefT1 = w.repo().getRef("t1"); - Ref head = w.repo().getRef("HEAD"); + Ref tagRefT1 = w.repo().findRef("t1"); + Ref head = w.repo().exactRef("HEAD"); assertEquals("head != t1", head.getObjectId(), tagRefT1.getObjectId()); w.commitEmpty("c2"); - Ref commitRefC2 = w.repo().getRef("HEAD"); + Ref commitRefC2 = w.repo().exactRef("HEAD"); List revList = w.git.revList("t1"); assertTrue("c1 not in revList", revList.contains(commitRefC1.getObjectId())); assertEquals("Wrong list size: " + revList, 1, revList.size()); @@ -4141,7 +4244,7 @@ public void test_revList_local_branch() throws Exception { assertEquals("Wrong list size: " + revList, 2, revList.size()); } - @Bug(20153) + @Issue("JENKINS-20153") public void test_checkoutBranch_null() throws Exception { w.init(); w.commitEmpty("c1"); @@ -4152,7 +4255,7 @@ public void test_checkoutBranch_null() throws Exception { assertEquals(w.head(),w.git.revParse(sha1)); - Ref head = w.repo().getRef("HEAD"); + Ref head = w.repo().exactRef("HEAD"); assertFalse(head.isSymbolic()); } @@ -4164,7 +4267,7 @@ private String formatBranches(List branches) { return Util.join(names,","); } - @Bug(18988) + @Issue("JENKINS-18988") public void test_localCheckoutConflict() throws Exception { w.init(); w.touch("foo","old"); @@ -4214,7 +4317,7 @@ public void test_isBareRepository_working_null() throws IOException, Interrupted assertFalse("null is a bare repository", w.igit().isBareRepository(null)); fail("Did not throw expected exception"); } catch (GitException ge) { - assertTrue("Wrong exception message: " + ge, ge.getMessage().contains("Not a git repository")); + assertExceptionMessageContains(ge, "not a git repository"); } } @@ -4225,7 +4328,7 @@ public void test_isBareRepository_bare_null() throws IOException, InterruptedExc assertTrue("null is not a bare repository", w.igit().isBareRepository(null)); fail("Did not throw expected exception"); } catch (GitException ge) { - assertTrue("Wrong exception message: " + ge, ge.getMessage().contains("Not a git repository")); + assertExceptionMessageContains(ge, "not a git repository"); } } @@ -4289,7 +4392,7 @@ public void test_isBareRepository_working_dot() throws IOException, InterruptedE fail("Did not throw expected exception"); } } catch (GitException ge) { - assertTrue("Wrong exception message: " + ge, ge.getMessage().contains("Not a git repository")); + assertExceptionMessageContains(ge, "not a git repository"); } } @@ -4325,7 +4428,7 @@ public void test_isBareRepository_bare_dot_git() throws IOException, Interrupted assertFalse("CliGitAPIImpl did not throw expected exception", w.igit() instanceof CliGitAPIImpl); } catch (GitException ge) { /* Only enters this path for CliGit */ - assertTrue("Wrong exception message: " + ge, ge.getMessage().contains("Not a git repository")); + assertExceptionMessageContains(ge, "not a git repository"); } } @@ -4340,7 +4443,7 @@ public void test_isBareRepository_working_no_such_location() throws IOException, assertFalse("CliGitAPIImpl did not throw expected exception", w.igit() instanceof CliGitAPIImpl); } catch (GitException ge) { /* Only enters this path for CliGit */ - assertTrue("Wrong exception message: " + ge, ge.getMessage().contains("Not a git repository")); + assertExceptionMessageContains(ge, "not a git repository"); } } @@ -4354,7 +4457,7 @@ public void test_isBareRepository_bare_no_such_location() throws IOException, In assertFalse("CliGitAPIImpl did not throw expected exception", w.igit() instanceof CliGitAPIImpl); } catch (GitException ge) { /* Only enters this path for CliGit */ - assertTrue("Wrong exception message: " + ge, ge.getMessage().contains("Not a git repository")); + assertExceptionMessageContains(ge, "not a git repository"); } } @@ -4539,13 +4642,8 @@ public void test_longpaths_disabled() throws Exception { /** * Test parsing of changelog with unicode characters in commit messages. */ + @Issue({"JENKINS-6203", "JENKINS-14798", "JENKINS-23091"}) public void test_unicodeCharsInChangelog() throws Exception { - - // Test for - // https://issues.jenkins-ci.org/browse/JENKINS-6203 - // https://issues.jenkins-ci.org/browse/JENKINS-14798 - // https://issues.jenkins-ci.org/browse/JENKINS-23091 - File tempRemoteDir = temporaryDirectoryAllocator.allocate(); extract(new ZipFile("src/test/resources/unicodeCharsInChangelogRepo.zip"), tempRemoteDir); File pathToTempRepo = new File(tempRemoteDir, "unicodeCharsInChangelogRepo"); @@ -4591,7 +4689,7 @@ public void test_git_init_creates_directory_if_needed() throws Exception { } } - @Bug(40023) + @Issue("JENKINS-40023") public void test_changelog_with_merge_commit_and_max_log_history() throws Exception { w.init(); w.commitEmpty("init"); @@ -4632,4 +4730,49 @@ private boolean isWindows() { return File.pathSeparatorChar==';'; } + private void withSystemLocaleReporting(String fileName, TestedCode code) throws Exception { + try { + code.run(); + } catch (GitException ge) { + // Exception message should contain the actual file name. + // It may just contain ? for characters that are not encoded correctly due to the system locale. + // If such a mangled file name is seen instead, throw a clear exception to indicate the root cause. + assertTrue("System locale does not support filename '" + fileName + "'", ge.getMessage().contains("?")); + // Rethrow exception for all other issues. + throw ge; + } + } + + @FunctionalInterface + interface TestedCode { + void run() throws Exception; + } + + private WorkingArea setupRepositoryWithSubmodule() throws Exception { + WorkingArea workingArea = new WorkingArea(); + + File repositoryDir = workingArea.file("dir-repository"); + File submoduleDir = workingArea.file("dir-submodule"); + + assertTrue("did not create dir " + repositoryDir.getName(), repositoryDir.mkdir()); + assertTrue("did not create dir " + submoduleDir.getName(), submoduleDir.mkdir()); + + WorkingArea submoduleWorkingArea = new WorkingArea(submoduleDir).init(); + + for (int commit = 1; commit <= 5; commit++) { + submoduleWorkingArea.touch("file", String.format("submodule content-%d", commit)); + submoduleWorkingArea.cgit().add("file"); + submoduleWorkingArea.cgit().commit(String.format("submodule commit-%d", commit)); + } + + WorkingArea repositoryWorkingArea = new WorkingArea(repositoryDir).init(); + + repositoryWorkingArea.commitEmpty("init"); + + repositoryWorkingArea.cgit().add("."); + repositoryWorkingArea.cgit().addSubmodule("file://" + submoduleDir.getAbsolutePath(), "submodule"); + repositoryWorkingArea.cgit().commit("submodule"); + + return workingArea; + } } diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/GitClientTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/GitClientTest.java index e8de01733a..5a80cc2321 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/GitClientTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/GitClientTest.java @@ -73,10 +73,10 @@ public class GitClientTest { private GitClient srcGitClient; /* commit known to exist in upstream. */ - final ObjectId upstreamCommit = ObjectId.fromString("f75720d5de9d79ab4be2633a21de23b3ccbf8ce3"); - final String upstreamCommitAuthor = "Teubel György"; - final String upsstreamCommitEmail = ""; - final ObjectId upstreamCommitPredecessor = ObjectId.fromString("867e5f148377fd5a6d96e5aafbdaac132a117a5a"); + private final ObjectId upstreamCommit = ObjectId.fromString("f75720d5de9d79ab4be2633a21de23b3ccbf8ce3"); + private final String upstreamCommitAuthor = "Teubel György"; + private final String upstreamCommitEmail = ""; + private final ObjectId upstreamCommitPredecessor = ObjectId.fromString("867e5f148377fd5a6d96e5aafbdaac132a117a5a"); /* URL of upstream (GitHub) repository. */ private final String upstreamRepoURL = "https://github.com/jenkinsci/git-client-plugin"; @@ -220,7 +220,7 @@ private ObjectId commitFile(final String path, final String content, final Strin return headList.get(0); } - public void createFile(String path, String content) throws Exception { + private void createFile(String path, String content) throws Exception { File aFile = new File(repoRoot, path); File parentDir = aFile.getParentFile(); if (parentDir != null) { @@ -257,6 +257,41 @@ private String randomEmail(String name) { return name.replaceAll(" ", ".") + "@middle.earth"; } + @Test + @Issue("JENKINS-29977") + /** + * Changelog was formatted on word boundary prior to + * 72 characters with git client plugin 2.0+ when using CLI git. + * Was not truncated by git client plugin using JGit (And Apache version). + * Rely on caller to truncate first line if desired. + * Matching change will be included in git plugin 4.0.0 + * to retain existing truncation behavior. + */ + public void testChangelogVeryLong() throws Exception { + + final String gitMessage = + "Uno Dos Tres Cuatro Cinco Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut " + + "posuere tellus eu efficitur tristique. In iaculis neque in dolor vulputate" + + "sollicitudin eget a quam. Donec finibus sapien quis lectus euismod facilisis. Integer" + + "massa purus, scelerisque id iaculis ut, blandit vitae velit. Pellentesque lobortis" + + "aliquet felis, vel laoreet ipsum tincidunt at. Mauris tellus est, cursus vitae ex" + + "eget, venenatis auctor eros. Sed sagittis porta odio. Donec ut interdum massa. Aliquam" + + "sagittis, mi sit amet sollicitudin elementum, velit quam eleifend nisl, in rhoncus" + + "felis nibh eu nibh. Class aptent taciti sociosqu ad litora torquent per conubia " + + "nostra, per inceptos himenaeos." + + "\nseis\n" + + "\nasfasfasfasf\n" + ; + final String content = String.format("A random UUID: %s\n", UUID.randomUUID().toString()); + ObjectId message = commitFile("One-File.txt", content, gitMessage); + + ChangelogCommand changelog = gitClient.changelog(); + StringWriter changelogStringWriter = new StringWriter(); + changelog.includes(message).to(changelogStringWriter).execute(); + assertThat(changelogStringWriter.toString(), containsString("Ut posuere")); + assertThat(changelogStringWriter.toString(), containsString("conubia nostra")); + } + @Test @Issue("JENKINS-39832") // Diagnostics of ChangelogCommand were insufficient public void testChangelogExceptionMessage() throws Exception { @@ -340,12 +375,16 @@ public void testNullChangelogDestinationExcludes() throws Exception { @Test @Issue("JENKINS-43198") public void testCleanSubdirGitignore() throws Exception { - final String filename = "this_is/not_ok/more/subdirs/file.txt"; - commitFile(".gitignore", "/this_is/not_ok\n", "set up gitignore"); - createFile(filename, "hi there"); - assertFileInWorkingDir(gitClient, filename); + final String filename1 = "this_is/not_ok/more/subdirs/file.txt"; + final String filename2 = "this_is_also/not_ok_either/more/subdirs/file.txt"; + commitFile(".gitignore", "/this_is/not_ok\n/this_is_also/not_ok_either\n", "set up gitignore"); + createFile(filename1, "hi there"); + createFile(filename2, "hi there"); + assertFileInWorkingDir(gitClient, filename1); + assertFileInWorkingDir(gitClient, filename2); gitClient.clean(); assertDirNotInWorkingDir(gitClient, "this_is"); + assertDirNotInWorkingDir(gitClient, "this_is_also"); } @Test @@ -395,7 +434,7 @@ public void testSetAuthor_String_String() throws Exception { @Test(expected = GitException.class) public void testCommitNotFoundException() throws GitException, InterruptedException { /* Search wrong repository for a commit */ - assertAuthor(upstreamCommitPredecessor, upstreamCommit, upstreamCommitAuthor, upsstreamCommitEmail); + assertAuthor(upstreamCommitPredecessor, upstreamCommit, upstreamCommitAuthor, upstreamCommitEmail); } @Test @@ -581,8 +620,6 @@ private void assertBranch(GitClient client, String branchName) throws Exception gitCmd.assertOutputContains(".*On branch.*" + branchName + ".*"); } - private int lastFetchPath = -1; - private void fetch(GitClient client, String remote, String firstRefSpec, String... optionalRefSpecs) throws Exception { List refSpecs = new ArrayList<>(); RefSpec refSpec = new RefSpec(firstRefSpec); @@ -590,8 +627,7 @@ private void fetch(GitClient client, String remote, String firstRefSpec, String. for (String refSpecString : optionalRefSpecs) { refSpecs.add(new RefSpec(refSpecString)); } - lastFetchPath = random.nextInt(2); - switch (lastFetchPath) { + switch (random.nextInt(2)) { default: case 0: if (remote.equals("origin")) { @@ -779,6 +815,13 @@ public void testCheckoutWithoutLFSWhenLFSNotAvailable() throws Exception { assertEquals("Incorrect non-LFS file contents in " + uuidFile, expectedContent, fileContent); } + /* + * JGit versions prior to 4.9.0 required a work around that the + * tags refspec had to be passed in addition to setting the + * FETCH_TAGS tagOpt. JGit 4.9.0 fixed that bug. This test would + * throw a DuplicateRef exception with JGit 4.9.0 prior to the + * removal of the work around (from JGitAPIImpl). + */ @Test public void testDeleteRef() throws Exception { assertThat(gitClient.getRefNames(""), is(empty())); @@ -848,7 +891,7 @@ public void testGetHeadRev_String_String_URI_Exception() throws Exception { public void testGetHeadRev_String_String_Empty_Result() throws Exception { String url = repoRoot.getAbsolutePath(); ObjectId nonExistent = gitClient.getHeadRev(url, "this branch doesn't exist"); - assertEquals(null, nonExistent); + assertNull(nonExistent); } @Test @@ -864,9 +907,8 @@ public void testGetRemoteReferences() throws Exception { String pattern = null; boolean headsOnly = false; // Need variations here boolean tagsOnly = false; // Need variations here - Map expResult = null; // Working here Map result = gitClient.getRemoteReferences(url, pattern, headsOnly, tagsOnly); - assertEquals(expResult, result); + assertNull(result); } @Issue("JENKINS-30589") @@ -1131,7 +1173,7 @@ public void testModifiedTrackedFilesReset() throws Exception { lastModifiedFile = file; } } - assertTrue("No files modified " + repoRoot, lastModifiedFile != null); + assertNotNull("No files modified " + repoRoot, lastModifiedFile); /* Checkout a new branch - verify no files retain modification */ gitClient.checkout().branch("master-" + randomUUID).ref(commitA.getName()).execute(); @@ -1178,8 +1220,7 @@ private void assertSubmoduleContents(GitClient client, String... directories) th dirList.add(dir.getName()); } } - assertThat(dirList, containsInAnyOrder(expectedDirList.toArray(new String[expectedDirList.size()]))); - assertThat(expectedDirList, containsInAnyOrder(dirList.toArray(new String[dirList.size()]))); + assertThat(dirList, containsInAnyOrder(expectedDirList.toArray())); } private void assertSubmoduleContents(String... directories) throws Exception { @@ -1525,22 +1566,15 @@ public void testSubmoduleClean() throws Exception { // Test may fail if updateSubmodule called with remoteTracking == true // and the remoteTracking argument is used in the updateSubmodule call updateSubmodule(upstream, branchName, null); - if (gitImplName.equals("git")) { - assertSubmoduleDirectories(gitClient, true, "firewall", "ntp", "sshkeys"); - assertSubmoduleContents("firewall", "ntp", "sshkeys"); - } else { - assertSubmoduleDirectories(gitClient, true, "firewall", "ntp"); // No renamed submodule - assertSubmoduleContents("firewall", "ntp"); // No renamed submodule - } + assertSubmoduleDirectories(gitClient, true, "firewall", "ntp", "sshkeys"); + assertSubmoduleContents("firewall", "ntp", "sshkeys"); final File firewallDir = new File(repoRoot, "modules/firewall"); final File firewallFile = File.createTempFile("untracked-", ".txt", firewallDir); final File ntpDir = new File(repoRoot, "modules/ntp"); final File ntpFile = File.createTempFile("untracked-", ".txt", ntpDir); - if (gitImplName.equals("git")) { - final File sshkeysDir = new File(repoRoot, "modules/sshkeys"); - final File sshkeysFile = File.createTempFile("untracked-", ".txt", sshkeysDir); - } + final File sshkeysDir = new File(repoRoot, "modules/sshkeys"); + final File sshkeysFile = File.createTempFile("untracked-", ".txt", sshkeysDir); assertStatusUntrackedContent(gitClient, true); @@ -1551,12 +1585,6 @@ public void testSubmoduleClean() throws Exception { /* GitClient submoduleClean expected to modify submodules */ boolean recursive = random.nextBoolean(); gitClient.submoduleClean(recursive); - if (!gitImplName.equals("git")) { - /* Fix damage done by JGit.submoduleClean() - * JGit won't leave repo clean, but does remove untracked content - */ - FileUtils.deleteQuietly(new File(repoRoot, "modules/sshkeys")); - } assertStatusUntrackedContent(gitClient, false); } @@ -1803,4 +1831,59 @@ public void testGetTags_ThreeTags() throws Exception { Set result = gitClient.getTags(); assertThat(result, containsInAnyOrder(expectedTag, expectedTag2, expectedTag3)); } + + private String truncateAtWord(String src, int maxLength) { + java.text.BreakIterator breakIterator = java.text.BreakIterator.getWordInstance(); + breakIterator.setText(src); + return src.substring(0, breakIterator.preceding(maxLength + 1)).trim(); + } + + private String wrapAtWord(String src, int maxLength) { + return org.apache.commons.text.WordUtils.wrap(src, maxLength); + } + + private String padLinesWithSpaces(String src, int spacePadCount) { + char[] paddingArray = new char[spacePadCount]; + Arrays.fill(paddingArray, ' '); + String padding = new String(paddingArray); + return padding + src.replace("\n", "\n" + padding).trim(); + } + + @Test + @Issue("JENKINS-29977") + public void testChangelogFirstLineTruncation() throws Exception { + // 1 2 3 4 5 6 7 8 + // 12345678901234567890123456789012345678901234567890123456789012345678901234567890 + final String longFirstLine = + "The first line of this commit message is longer than 72 characters to show JENKINS-29977"; + final String longBody = + "The body of this commit message is also longer than 72 characters though that is not part of JENKINS-29977"; + // Intentionally randomize whether the commit message body ends with a newline character + final String commitMessage = longFirstLine + "\n\n" + longBody + (random.nextBoolean() ? "\n" : ""); + final ObjectId commit = commitOneFile(commitMessage); + ChangelogCommand changelog = gitClient.changelog(); + StringWriter changelogStringWriter = new StringWriter(); + changelog.includes(commit).to(changelogStringWriter).execute(); + + final String truncatedFirstLine = truncateAtWord(longFirstLine, 72) + "\n"; + final String truncatedBody = truncateAtWord(longBody, 72) + "\n"; + + final String wrappedFirstLine = wrapAtWord(longFirstLine, 72); + final String wrappedBody = wrapAtWord(longBody, 72); + + // Truncated lines are NOT included in the changelog + assertThat(changelogStringWriter.toString(), not(containsString(truncatedFirstLine))); + assertThat(changelogStringWriter.toString(), not(containsString(truncatedBody))); + + // Wrapped lines are NOT included in the changelog + assertThat(changelogStringWriter.toString(), not(containsString(padLinesWithSpaces(wrappedFirstLine, 4)))); + assertThat(changelogStringWriter.toString(), not(containsString(padLinesWithSpaces(wrappedBody, 4)))); + + // Unmodified lines are included in the changelog + assertThat(changelogStringWriter.toString(), containsString(longFirstLine)); + assertThat(changelogStringWriter.toString(), containsString(longBody)); + + // Entire unmodified commit message is included in the changelog + assertThat(changelogStringWriter.toString(), containsString(padLinesWithSpaces(commitMessage, 4))); + } } diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/GitURIRequirementsBuilderTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/GitURIRequirementsBuilderTest.java index e0f952ba6a..327eadc34e 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/GitURIRequirementsBuilderTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/GitURIRequirementsBuilderTest.java @@ -550,7 +550,7 @@ public void smokes() throws Exception { } - T firstOrNull(List list, Class type) { + private static T firstOrNull(List list, Class type) { for (Object i: list) { if (type.isInstance(i)) return type.cast(i); diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/LegacyCompatibleGitAPIImplTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/LegacyCompatibleGitAPIImplTest.java index 954f6ee59d..252a573e04 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/LegacyCompatibleGitAPIImplTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/LegacyCompatibleGitAPIImplTest.java @@ -210,8 +210,8 @@ public void testGetTagsOnCommit() throws Exception { List result = myGit.getTagsOnCommit(uniqueTagName); myGit.deleteTag(uniqueTagName); assertFalse("Tag list empty for " + uniqueTagName, result.isEmpty()); - assertEquals("Unexpected SHA1 for commit: " + result.get(0).getCommitMessage(), null, result.get(0).getCommitSHA1()); - assertEquals("Unexpected message for commit: " + result.get(0).getCommitSHA1(), null, result.get(0).getCommitMessage()); + assertNull("Unexpected SHA1 for commit: " + result.get(0).getCommitMessage(), result.get(0).getCommitSHA1()); + assertNull("Unexpected message for commit: " + result.get(0).getCommitSHA1(), result.get(0).getCommitMessage()); } @Test diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/LogHandler.java b/src/test/java/org/jenkinsci/plugins/gitclient/LogHandler.java index a1a342d606..41661f63d7 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/LogHandler.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/LogHandler.java @@ -29,20 +29,15 @@ public void close() throws SecurityException { messages = new ArrayList<>(); } - /* package */ List getMessages() { + List getMessages() { return messages; } - /* package */ boolean containsMessageSubstring(String messageSubstring) { - for (String message : messages) { - if (message.contains(messageSubstring)) { - return true; - } - } - return false; + boolean containsMessageSubstring(String messageSubstring) { + return messages.stream().anyMatch(message -> message.contains(messageSubstring)); } - /* package */ List getTimeouts() { + List getTimeouts() { List timeouts = new ArrayList<>(); for (String message : getMessages()) { int start = message.indexOf(CliGitAPIImpl.TIMEOUT_LOG_PREFIX); diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/LogHandlerTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/LogHandlerTest.java index df07d9fcef..ae9a46323c 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/LogHandlerTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/LogHandlerTest.java @@ -11,9 +11,6 @@ public class LogHandlerTest { private LogHandler handler; - public LogHandlerTest() { - } - @Before public void setUp() { handler = new LogHandler(); diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/NetrcTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/NetrcTest.java index ed1fcfa6ac..64a46fde13 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/NetrcTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/NetrcTest.java @@ -29,8 +29,7 @@ public class NetrcTest private String testFilePath_1a; private String testFilePath_2; - - enum TestHost { + private enum TestHost { H1_01("1-srvr-lp.example.com", "jenkins", "pw4jenkins"), H1_02("2-ldap-lp.example.com", "ldap", "pw4ldap"), H1_03("3-jenk-pl.example.com", "jenkins", "jenkinspwd"), @@ -56,9 +55,9 @@ enum TestHost { H2_03("builder3.example.com", null, null), H2_04("builder4.example.com", "jenk", "myvoice"); - public String machine; - public String login; - public String password; + private String machine; + private String login; + private String password; private TestHost(String _machine, String _login, String _password) { diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/PushTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/PushTest.java index 889ad5e965..4e8340c492 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/PushTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/PushTest.java @@ -8,6 +8,7 @@ import java.util.Collection; import java.util.List; import java.util.Random; +import static java.util.stream.Collectors.toList; import hudson.model.TaskListener; import hudson.plugins.git.Branch; @@ -15,7 +16,6 @@ import hudson.util.StreamTaskListener; import org.apache.commons.io.FileUtils; -import com.google.common.io.Files; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.transport.RefSpec; @@ -23,12 +23,15 @@ import org.junit.After; import org.junit.AfterClass; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; import org.junit.rules.TestName; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -69,6 +72,12 @@ public class PushTest { @Rule public TestName name = new TestName(); + @ClassRule + public static TemporaryFolder staticTemporaryFolder = new TemporaryFolder(); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + public PushTest(String gitImpl, String branchName, String refSpec, Class expectedException) { this.gitImpl = gitImpl; this.branchName = branchName; @@ -142,7 +151,7 @@ public void createWorkingRepository() throws IOException, InterruptedException, hudson.EnvVars env = new hudson.EnvVars(); TaskListener listener = StreamTaskListener.fromStderr(); List refSpecs = new ArrayList<>(); - workingRepo = Files.createTempDir(); + workingRepo = temporaryFolder.newFolder(); workingGitClient = Git.with(listener, env).in(workingRepo).using(gitImpl).getClient(); workingGitClient.clone_() .url(bareRepo.getAbsolutePath()) @@ -170,7 +179,6 @@ public void verifyPushResultAndDeleteDirectory() throws GitException, Interrupte assertNotEquals(previousCommit, workingCommit); assertNotEquals(previousCommit, latestBareHead); } - FileUtils.deleteDirectory(workingRepo.getAbsoluteFile()); } @BeforeClass @@ -185,7 +193,7 @@ public static void createBareRepository() throws Exception { String gitImpl = gitImplementations[random.nextInt(gitImplementations.length)]; /* Create the bare repository */ - bareRepo = Files.createTempDir(); + bareRepo = staticTemporaryFolder.newFolder(); bareURI = new URIish(bareRepo.getAbsolutePath()); hudson.EnvVars env = new hudson.EnvVars(); TaskListener listener = StreamTaskListener.fromStderr(); @@ -193,7 +201,7 @@ public static void createBareRepository() throws Exception { bareGitClient.init_().workspace(bareRepo.getAbsolutePath()).bare(true).execute(); /* Clone the bare repository into a working copy */ - File cloneRepo = Files.createTempDir(); + File cloneRepo = staticTemporaryFolder.newFolder(); GitClient cloneGitClient = Git.with(listener, env).in(cloneRepo).using(gitImpl).getClient(); cloneGitClient.clone_() .url(bareRepo.getAbsolutePath()) @@ -217,18 +225,10 @@ public static void createBareRepository() throws Exception { cloneGitClient.push().to(bareURI).ref("HEAD:" + branchName).execute(); } - /* Remove the clone */ - FileUtils.deleteDirectory(cloneRepo); - /* Remember the SHA1 of the first commit */ bareFirstCommit = bareGitClient.getHeadRev(bareRepo.getAbsolutePath(), "master"); } - @AfterClass - public static void removeBareRepository() throws IOException { - FileUtils.deleteDirectory(bareRepo); - } - protected void checkoutBranchAndCommitFile() throws GitException, InterruptedException, IOException { previousCommit = checkoutBranch(false); workingCommit = commitFileToCurrentBranch(); @@ -239,12 +239,15 @@ protected void checkoutOldBranchAndCommitFile() throws GitException, Interrupted workingCommit = commitFileToCurrentBranch(); } + private Collection getBranchNames(List branches) { + return branches.stream().map(Branch::getName).collect(toList()); + } + private ObjectId checkoutBranch(boolean useOldCommit) throws GitException, InterruptedException { /* Checkout branchName */ workingGitClient.checkoutBranch(branchName, "origin/" + branchName + (useOldCommit ? "^" : "")); List branches = workingGitClient.getBranchesContaining(branchName, false); - assertEquals("Wrong working branch count", 1, branches.size()); - assertEquals("Wrong working branch name", branchName, branches.get(0).getName()); + assertThat(getBranchNames(branches), contains(branchName)); return bareGitClient.getHeadRev(bareRepo.getAbsolutePath(), branchName); } diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/RemotingTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/RemotingTest.java index 9b5f65672b..df84d1b32c 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/RemotingTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/RemotingTest.java @@ -1,5 +1,7 @@ package org.jenkinsci.plugins.gitclient; +import static org.junit.Assert.*; + import hudson.FilePath; import hudson.model.Computer; import hudson.model.StreamBuildListener; @@ -8,8 +10,10 @@ import hudson.slaves.DumbSlave; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Repository; -import org.jvnet.hudson.test.HudsonTestCase; - +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.jvnet.hudson.test.JenkinsRule; import java.io.File; import java.io.IOException; import org.jenkinsci.remoting.RoleChecker; @@ -17,15 +21,22 @@ /** * @author Kohsuke Kawaguchi */ -public class RemotingTest extends HudsonTestCase { +public class RemotingTest { + + @ClassRule + public static JenkinsRule j = new JenkinsRule(); + + @ClassRule + public static TemporaryFolder tempFolder = new TemporaryFolder(); + /** * Makes sure {@link GitClient} is remotable. */ + @Test public void testRemotability() throws Exception { - DumbSlave s = createSlave(); + DumbSlave s = j.createSlave(); - File dir = createTmpDir(); - final GitClient jgit = new JGitAPIImpl(dir,StreamBuildListener.fromStdout()); + GitClient jgit = new JGitAPIImpl(tempFolder.getRoot(), StreamBuildListener.fromStdout()); Computer c = s.toComputer(); c.connect(false).get(); @@ -34,7 +45,10 @@ public void testRemotability() throws Exception { channel.close(); } - private static class Work implements Callable { + private static class Work implements Callable { + + private static final long serialVersionUID = 1L; + private final GitClient git; private static boolean cliGitDefaultsSet = false; @@ -47,21 +61,24 @@ private void setCliGitDefaults() throws Exception { cliGitDefaultsSet = true; } - public Work(GitClient git) throws Exception { + private Work(GitClient git) throws Exception { setCliGitDefaults(); this.git = git; } + @Override public Void call() throws IOException { try { git.init(); git.getWorkTree().child("foo").touch(0); git.add("foo"); PersonIdent alice = new PersonIdent("alice", "alice@jenkins-ci.org"); - git.commit("committing changes", alice, alice); + git.setAuthor(alice); + git.setCommitter(alice); + git.commit("committing changes"); FilePath ws = git.withRepository(new RepositoryCallableImpl()); - assertEquals(ws,git.getWorkTree()); + assertEquals(ws, git.getWorkTree()); return null; } catch (InterruptedException e) { @@ -69,8 +86,6 @@ public Void call() throws IOException { } } - private static final long serialVersionUID = 1L; - @Override public void checkRoles(RoleChecker rc) throws SecurityException { throw new UnsupportedOperationException("unexpected call to checkRoles in private static Work class"); @@ -78,11 +93,13 @@ public void checkRoles(RoleChecker rc) throws SecurityException { } private static class RepositoryCallableImpl implements RepositoryCallback { - public FilePath invoke(Repository repo, VirtualChannel channel) throws IOException, InterruptedException { + + private static final long serialVersionUID = 1L; + + @Override + public FilePath invoke(Repository repo, VirtualChannel channel) { assertNotNull(repo); return new FilePath(repo.getWorkTree()); } - - private static final long serialVersionUID = 1L; } } diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/StringSharesPrefix.java b/src/test/java/org/jenkinsci/plugins/gitclient/StringSharesPrefix.java index 584645cbfb..2a50cbccf7 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/StringSharesPrefix.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/StringSharesPrefix.java @@ -6,7 +6,7 @@ /** * Tests if the argument shares a prefix. */ -public class StringSharesPrefix extends SubstringMatcher { +class StringSharesPrefix extends SubstringMatcher { public StringSharesPrefix(String substring) { super(substring); } @Override @@ -32,5 +32,5 @@ protected String relationship() { * the substring that the returned matcher will expect to share a * prefix of any examined string */ - public static Matcher sharesPrefix(String prefix) { return new StringSharesPrefix(prefix); } + static Matcher sharesPrefix(String prefix) { return new StringSharesPrefix(prefix); } } diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/WarnTempDirValueTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/WarnTempDirValueTest.java index 6eec4d5364..35b5fa16dc 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/WarnTempDirValueTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/WarnTempDirValueTest.java @@ -19,7 +19,7 @@ import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import org.jvnet.hudson.test.Bug; +import org.jvnet.hudson.test.Issue; /** * The msysgit implementation (through at least 1.9.0) fails some credential @@ -92,7 +92,7 @@ public void noWarningForDefaultValue() throws IOException, InterruptedException } @Test - @Bug(22706) + @Issue("JENKINS-22706") public void warnWhenValueContainsSpaceCharacter() throws IOException, InterruptedException { EnvVars env = new hudson.EnvVars(); assertFalse(env.get(envVarName, "/tmp").contains(" ")); diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/jgit/PreemptiveAuthHttpClientConnectionTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/jgit/PreemptiveAuthHttpClientConnectionTest.java index 2e813bf601..aa781ca2a5 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/jgit/PreemptiveAuthHttpClientConnectionTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/jgit/PreemptiveAuthHttpClientConnectionTest.java @@ -1,8 +1,9 @@ package org.jenkinsci.plugins.gitclient.jgit; +import static org.junit.Assert.*; + import org.apache.http.auth.NTCredentials; import org.eclipse.jgit.transport.URIish; -import org.junit.Assert; import org.junit.Test; /** @@ -15,7 +16,7 @@ public class PreemptiveAuthHttpClientConnectionTest { final URIish actual = PreemptiveAuthHttpClientConnection.goUp(input); - Assert.assertEquals(null, actual); + assertEquals(null, actual); } @Test public void goUp_slash() throws Exception { @@ -23,7 +24,7 @@ public class PreemptiveAuthHttpClientConnectionTest { final URIish actual = PreemptiveAuthHttpClientConnection.goUp(input); - Assert.assertEquals(null, actual); + assertEquals(null, actual); } @Test public void goUp_slashSlash() throws Exception { @@ -31,8 +32,8 @@ public class PreemptiveAuthHttpClientConnectionTest { final URIish actual = PreemptiveAuthHttpClientConnection.goUp(input); - Assert.assertNotNull(actual); - Assert.assertEquals("http://example.com", actual.toString()); + assertNotNull(actual); + assertEquals("http://example.com", actual.toString()); } @Test public void goUp_one() throws Exception { @@ -40,8 +41,8 @@ public class PreemptiveAuthHttpClientConnectionTest { final URIish actual = PreemptiveAuthHttpClientConnection.goUp(input); - Assert.assertNotNull(actual); - Assert.assertEquals("http://example.com", actual.toString()); + assertNotNull(actual); + assertEquals("http://example.com", actual.toString()); } @Test public void goUp_oneSlash() throws Exception { @@ -49,8 +50,8 @@ public class PreemptiveAuthHttpClientConnectionTest { final URIish actual = PreemptiveAuthHttpClientConnection.goUp(input); - Assert.assertNotNull(actual); - Assert.assertEquals("http://example.com", actual.toString()); + assertNotNull(actual); + assertEquals("http://example.com", actual.toString()); } @Test public void goUp_oneSlashTwo() throws Exception { @@ -58,8 +59,8 @@ public class PreemptiveAuthHttpClientConnectionTest { final URIish actual = PreemptiveAuthHttpClientConnection.goUp(input); - Assert.assertNotNull(actual); - Assert.assertEquals("http://example.com/one", actual.toString()); + assertNotNull(actual); + assertEquals("http://example.com/one", actual.toString()); } @Test public void goUp_oneSlashSlashTwoSlash() throws Exception { @@ -67,8 +68,8 @@ public class PreemptiveAuthHttpClientConnectionTest { final URIish actual = PreemptiveAuthHttpClientConnection.goUp(input); - Assert.assertNotNull(actual); - Assert.assertEquals("http://example.com/one/", actual.toString()); + assertNotNull(actual); + assertEquals("http://example.com/one/", actual.toString()); } @Test public void goUp_oneSlashTwoSlash() throws Exception { @@ -76,17 +77,17 @@ public class PreemptiveAuthHttpClientConnectionTest { final URIish actual = PreemptiveAuthHttpClientConnection.goUp(input); - Assert.assertNotNull(actual); - Assert.assertEquals("http://example.com/one", actual.toString()); + assertNotNull(actual); + assertEquals("http://example.com/one", actual.toString()); } private static void createNTCredentials(final String inputUserName, final String inputPassword, final String expectedDomain, final String expectedUserName, final String expectedPassword) { final NTCredentials actual = PreemptiveAuthHttpClientConnection.createNTCredentials(inputUserName, inputPassword); - Assert.assertEquals(expectedDomain, actual.getDomain()); - Assert.assertEquals(expectedUserName, actual.getUserName()); - Assert.assertEquals(expectedPassword, actual.getPassword()); + assertEquals(expectedDomain, actual.getDomain()); + assertEquals(expectedUserName, actual.getUserName()); + assertEquals(expectedPassword, actual.getPassword()); } @Test public void createNTCredentials_plainUser() throws Exception { diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/trilead/CredentialsProviderImplTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/trilead/CredentialsProviderImplTest.java index 19d39c8bda..63f6c46e56 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/trilead/CredentialsProviderImplTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/trilead/CredentialsProviderImplTest.java @@ -20,7 +20,6 @@ public class CredentialsProviderImplTest { private CredentialsProviderImpl provider; private TaskListener listener; - private StandardUsernameCredentials cred; private final String USER_NAME = "user-name"; private final URIish uri; private final String SECRET_VALUE = "secret-credentials-provider-impl-test"; @@ -36,7 +35,7 @@ public CredentialsProviderImplTest() throws URISyntaxException { public void setUp() { Secret secret = Secret.fromString(SECRET_VALUE); listener = StreamTaskListener.fromStdout(); - cred = new StandardUsernamePasswordCredentialsImpl(USER_NAME, secret); + StandardUsernameCredentials cred = new StandardUsernamePasswordCredentialsImpl(USER_NAME, secret); provider = new CredentialsProviderImpl(listener, cred); } diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/trilead/StandardUsernamePasswordCredentialsImpl.java b/src/test/java/org/jenkinsci/plugins/gitclient/trilead/StandardUsernamePasswordCredentialsImpl.java index 4aa788983f..b0662f805b 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/trilead/StandardUsernamePasswordCredentialsImpl.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/trilead/StandardUsernamePasswordCredentialsImpl.java @@ -5,7 +5,7 @@ import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import hudson.util.Secret; -/* package */ class StandardUsernamePasswordCredentialsImpl implements StandardUsernamePasswordCredentials { +class StandardUsernamePasswordCredentialsImpl implements StandardUsernamePasswordCredentials { private final String userName; private final Secret password;