diff --git a/src/main/java/hudson/plugins/git/GitAPI.java b/src/main/java/hudson/plugins/git/GitAPI.java index e5c44ff8ea..e3ef43d0eb 100644 --- a/src/main/java/hudson/plugins/git/GitAPI.java +++ b/src/main/java/hudson/plugins/git/GitAPI.java @@ -173,6 +173,12 @@ public GitClient subGit(String subdir) { return Git.USE_CLI ? super.subGit(subdir) : jgit.subGit(subdir); } + /** {@inheritDoc} */ + @Override + public GitClient newGit(String somedir) { + return Git.USE_CLI ? super.newGit(somedir) : jgit.newGit(somedir); + } + /** {@inheritDoc} */ @Override public void setRemoteUrl(String name, String url) throws GitException, InterruptedException { diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java index 90df624b3e..c51beb0647 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java @@ -349,6 +349,12 @@ public GitClient subGit(String subdir) { return new CliGitAPIImpl(gitExe, new File(workspace, subdir), listener, environment); } + /** {@inheritDoc} */ + @Override + public GitClient newGit(String somedir) { + return new CliGitAPIImpl(gitExe, new File(somedir), listener, environment); + } + /** * Initialize an empty repository for further git operations. * @@ -828,33 +834,58 @@ public void execute() throws GitException, InterruptedException { } if (reference != null && !reference.isEmpty()) { - File referencePath = new File(reference); - if (!referencePath.exists()) { - listener.getLogger().println("[WARNING] Reference path does not exist: " + reference); - } else if (!referencePath.isDirectory()) { - listener.getLogger().println("[WARNING] Reference path is not a directory: " + reference); + if (isParameterizedReferenceRepository(reference)) { + // LegacyCompatibleGitAPIImpl.java has a logging trace, but not into build console via listener + listener.getLogger() + .println("[INFO] The git reference repository path '" + reference + "' " + + "is parameterized, it may take a few git queries logged " + + "below to resolve it into a particular directory name"); + } + File referencePath = findParameterizedReferenceRepository(reference, url); + if (referencePath == null) { + listener.getLogger() + .println("[ERROR] Could not make File object from reference path, " + + "skipping its use: " + reference); } else { - // reference path can either be a normal or a base repository - File objectsPath = new File(referencePath, ".git/objects"); - if (!objectsPath.isDirectory()) { - // reference path is bare repo - objectsPath = new File(referencePath, "objects"); + if (!referencePath.getPath().equals(reference)) { + // Note: both these logs are needed, they are used in selftest + String msg = "Parameterized reference path "; + msg += "'" + reference + "'"; + msg += " replaced with: "; + msg += "'" + referencePath.getPath() + "'"; + if (referencePath.exists()) { + listener.getLogger().println("[WARNING] " + msg); + } else { + listener.getLogger().println("[WARNING] " + msg + " does not exist"); + } + reference = referencePath.getPath(); } - if (!objectsPath.isDirectory()) { - listener.getLogger() - .println( - "[WARNING] Reference path does not contain an objects directory (not a git repo?): " - + objectsPath); + + if (!referencePath.exists()) { + listener.getLogger().println("[WARNING] Reference path does not exist: " + reference); + } else if (!referencePath.isDirectory()) { + listener.getLogger().println("[WARNING] Reference path is not a directory: " + reference); } else { - File alternates = new File(workspace, ".git/objects/info/alternates"); - try (PrintWriter w = new PrintWriter(alternates, Charset.defaultCharset())) { - String absoluteReference = - objectsPath.getAbsolutePath().replace('\\', '/'); - listener.getLogger().println("Using reference repository: " + reference); - // git implementations on windows also use - w.print(absoluteReference); - } catch (IOException e) { - listener.error("Failed to setup reference"); + File objectsPath = getObjectsFile(referencePath); + if (objectsPath == null || !objectsPath.isDirectory()) { + listener.getLogger() + .println("[WARNING] Reference path does not contain an objects directory " + + "(not a git repo?): " + objectsPath); + } else { + // Go behind git's back to write a meta file in new workspace + File alternates = new File(workspace, ".git/objects/info/alternates"); + try (PrintWriter w = new PrintWriter( + alternates, Charset.defaultCharset().toString())) { + String absoluteReference = + objectsPath.getAbsolutePath().replace('\\', '/'); + listener.getLogger().println("Using reference repository: " + reference); + // git implementations on windows also use + w.print(absoluteReference); + } catch (UnsupportedEncodingException ex) { + listener.error("Default character set is an unsupported encoding"); + } catch (FileNotFoundException e) { + listener.error("Failed to setup reference"); + } } } } @@ -1484,14 +1515,16 @@ public void execute() throws GitException, InterruptedException { } } if ((ref != null) && !ref.isEmpty()) { - File referencePath = new File(ref); - if (!referencePath.exists()) { - listener.getLogger().println("[WARNING] Reference path does not exist: " + ref); - } else if (!referencePath.isDirectory()) { - listener.getLogger().println("[WARNING] Reference path is not a directory: " + ref); - } else { - args.add("--reference", ref); - } + if (!isParameterizedReferenceRepository(ref)) { + File referencePath = new File(ref); + if (!referencePath.exists()) { + listener.getLogger().println("[WARNING] Reference path does not exist: " + ref); + } else if (!referencePath.isDirectory()) { + listener.getLogger().println("[WARNING] Reference path is not a directory: " + ref); + } else { + args.add("--reference", ref); + } + } // else handled below in per-module loop } if (shallow) { if (depth == null) { @@ -1544,9 +1577,34 @@ public void execute() throws GitException, InterruptedException { listener.error("Invalid repository for " + sModuleName); throw new GitException("Invalid repository for " + sModuleName); } + String strURIish = urIish.toPrivateString(); + + if (isParameterizedReferenceRepository(ref)) { + File referencePath = findParameterizedReferenceRepository(ref, strURIish); + if (referencePath == null) { + listener.getLogger() + .println("[ERROR] Could not make File object from reference path, " + + "skipping its use: " + ref); + } else { + String expRef = null; + if (referencePath.getPath().equals(ref)) { + expRef = ref; + } else { + expRef = referencePath.getPath(); + expRef += " (expanded from " + ref + ")"; + } + if (!referencePath.exists()) { + listener.getLogger().println("[WARNING] Reference path does not exist: " + expRef); + } else if (!referencePath.isDirectory()) { + listener.getLogger().println("[WARNING] Reference path is not a directory: " + expRef); + } else { + args.add("--reference", referencePath.getPath()); + } + } + } // Find credentials for this URL - StandardCredentials cred = credentials.get(urIish.toPrivateString()); + StandardCredentials cred = credentials.get(strURIish); if (parentCredentials) { String parentUrl = getRemoteUrl(getDefaultRemote()); URIish parentUri = null; @@ -1656,6 +1714,62 @@ public void setSubmoduleUrl(String name, String url) throws GitException, Interr return StringUtils.trim(firstLine(result)); } + /** {@inheritDoc} */ + @Override + public @CheckForNull Map getRemoteUrls() throws GitException, InterruptedException { + String result = launchCommand("config", "--local", "--list"); + Map uriNames = new HashMap<>(); + for (String line : result.split("\\R+")) { + line = StringUtils.trim(line); + if (!line.startsWith("remote.") || !line.contains(".url=")) { + continue; + } + + String remoteName = StringUtils.substringBetween(line, "remote.", ".url="); + String remoteUri = StringUtils.substringAfter(line, ".url="); + + // If uri String values end up identical, Map only stores one entry + uriNames.put(remoteUri, remoteName); + + try { + URI u = new URI(remoteUri); + uriNames.put(u.toASCIIString(), remoteName); + URI uSafe = new URI(u.getScheme(), u.getHost(), u.getPath(), u.getFragment()); + uriNames.put(uSafe.toString(), remoteName); + uriNames.put(uSafe.toASCIIString(), remoteName); + } catch (URISyntaxException ue) { + } // ignore, move along + } + return uriNames; + } + + /** {@inheritDoc} */ + @Override + public @CheckForNull Map getRemotePushUrls() throws GitException, InterruptedException { + String result = launchCommand("config", "--local", "--list"); + Map uriNames = new HashMap<>(); + for (String line : result.split("\\R+")) { + line = StringUtils.trim(line); + if (!line.startsWith("remote.") || !line.contains(".pushurl=")) { + continue; + } + + String remoteName = StringUtils.substringBetween(line, "remote.", ".pushurl="); + String remoteUri = StringUtils.substringAfter(line, ".pushurl="); + uriNames.put(remoteUri, remoteName); + + try { + URI u = new URI(remoteUri); + uriNames.put(u.toASCIIString(), remoteName); + URI uSafe = new URI(u.getScheme(), u.getHost(), u.getPath(), u.getFragment()); + uriNames.put(uSafe.toString(), remoteName); + uriNames.put(uSafe.toASCIIString(), remoteName); + } catch (URISyntaxException ue) { + } // ignore, move along + } + return uriNames; + } + /** {@inheritDoc} */ @Override public void setRemoteUrl(String name, String url) throws GitException, InterruptedException { @@ -2843,8 +2957,16 @@ private String launchCommandIn(ArgumentListBuilder args, File workDir, EnvVars e } if (status != 0) { - throw new GitException("Command \"" + command + "\" returned status code " + status + ":\nstdout: " - + stdout + "\nstderr: " + stderr); + if (workDir == null) + workDir = java.nio.file.Paths.get(".") + .toAbsolutePath() + .normalize() + .toFile(); + throw new GitException("Command \"" + command + + "\" executed in workdir \"" + workDir.toString() + + "\" returned status code " + status + + ":\nstdout: " + stdout + + "\nstderr: " + stderr); } return stdout; diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/GitClient.java b/src/main/java/org/jenkinsci/plugins/gitclient/GitClient.java index 4f375acb9a..6e5a498de3 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/GitClient.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/GitClient.java @@ -221,6 +221,28 @@ public interface GitClient { */ String getRemoteUrl(String name) throws GitException, InterruptedException; + /** + * getRemoteUrls. + * + * @return a Map where String keys represent URIs (with and + * without passwords, if any; ASCII or not, if + * applicable) for all remotes configured in this + * repository/workspace, and values represent names. + * There may be several URIs corresponding to same name. + */ + public Map getRemoteUrls() throws GitException, InterruptedException; + + /** + * getRemotePushUrls. + * + * @return a Map where String keys represent push-only URIs + * (with and without passwords, if any; ASCII or not, + * if applicable) for all remotes configured in this + * repository/workspace, and values represent names. + * There may be several URIs corresponding to same name. + */ + public Map getRemotePushUrls() throws GitException, InterruptedException; + /** * For a given repository, set a remote's URL * @@ -697,12 +719,23 @@ Map getRemoteSymbolicReferences(String remoteRepoUrl, String pat */ List revList(String ref) throws GitException, InterruptedException; + // --- new instance of same applied class + + /** + * newGit. + * + * @return an {@link IGitAPI} implementation to manage another git repository + * with same general settings and implementation as the current one. + * @param somedir a {@link java.lang.String} object. + */ + GitClient newGit(String somedir); + // --- submodules /** * subGit. * - * @return a IGitAPI implementation to manage git submodule repository + * @return an {@link IGitAPI} implementation to manage git submodule repository * @param subdir a {@link java.lang.String} object. */ GitClient subGit(String subdir); diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java index 7bbccc2db9..13a0d6f027 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java @@ -337,6 +337,12 @@ public GitClient subGit(String subdir) { return new JGitAPIImpl(new File(workspace, subdir), listener); } + /** {@inheritDoc} */ + @Override + public GitClient newGit(String somedir) { + return new JGitAPIImpl(new File(somedir), listener); + } + /** {@inheritDoc} */ @Override public void setAuthor(String name, String email) throws GitException { @@ -1123,6 +1129,50 @@ public String getRemoteUrl(String name) throws GitException { } } + /** {@inheritDoc} */ + @Override + public Map getRemoteUrls() throws GitException, InterruptedException { + Map uriNames = new HashMap<>(); + try (Repository repo = getRepository()) { + Config c = repo.getConfig(); + for (RemoteConfig rc : RemoteConfig.getAllRemoteConfigs(c)) { + String remoteName = rc.getName(); + for (URIish u : rc.getURIs()) { + // If uri String values end up identical, Map only stores one entry + uriNames.put(u.toString(), remoteName); + uriNames.put(u.toPrivateString(), remoteName); + uriNames.put(u.toASCIIString(), remoteName); + uriNames.put(u.toPrivateASCIIString(), remoteName); + } + } + } catch (URISyntaxException ue) { + throw new GitException(ue.toString()); + } + return uriNames; + } + + /** {@inheritDoc} */ + @Override + public Map getRemotePushUrls() throws GitException, InterruptedException { + Map uriNames = new HashMap<>(); + try (Repository repo = getRepository()) { + Config c = repo.getConfig(); + for (RemoteConfig rc : RemoteConfig.getAllRemoteConfigs(c)) { + String remoteName = rc.getName(); + for (URIish u : rc.getPushURIs()) { + // If uri String values end up identical, Map only stores one entry + uriNames.put(u.toString(), remoteName); + uriNames.put(u.toPrivateString(), remoteName); + uriNames.put(u.toASCIIString(), remoteName); + uriNames.put(u.toPrivateASCIIString(), remoteName); + } + } + } catch (URISyntaxException ue) { + throw new GitException(ue.toString()); + } + return uriNames; + } + /** * getRepository. * @@ -1642,36 +1692,60 @@ public void execute() throws GitException { // the repository builder does not create the alternates file if (reference != null && !reference.isEmpty()) { - File referencePath = new File(reference); - if (!referencePath.exists()) { - listener.getLogger().println("[WARNING] Reference path does not exist: " + reference); - } else if (!referencePath.isDirectory()) { - listener.getLogger().println("[WARNING] Reference path is not a directory: " + reference); + // Note: keep in sync with similar logic in CliGitAPIImpl.java + if (isParameterizedReferenceRepository(reference)) { + // LegacyCompatibleGitAPIImpl.java has a logging trace, + // but not into build console via listener + listener.getLogger() + .println("[INFO] The git reference repository path '" + reference + "' " + + "is parameterized, it may take a few git queries logged " + + "below to resolve it into a particular directory name"); + } + File referencePath = findParameterizedReferenceRepository(reference, url); + if (referencePath == null) { + listener.getLogger() + .println("[ERROR] Could not make File object from reference path, " + + "skipping its use: " + reference); } else { - // reference path can either be a normal or a base repository - File objectsPath = new File(referencePath, ".git/objects"); - if (!objectsPath.isDirectory()) { - // reference path is bare repo - objectsPath = new File(referencePath, "objects"); + if (!referencePath.getPath().equals(reference)) { + // Note: both these logs are needed, they are used in selftest + String msg = "Parameterized reference path "; + msg += "'" + reference + "'"; + msg += " replaced with: "; + msg += "'" + referencePath.getPath() + "'"; + if (referencePath.exists()) { + listener.getLogger().println("[WARNING] " + msg); + } else { + listener.getLogger().println("[WARNING] " + msg + " does not exist"); + } + reference = referencePath.getPath(); } - if (!objectsPath.isDirectory()) { + + if (!referencePath.exists()) { + listener.getLogger().println("[WARNING] Reference path does not exist: " + reference); + } else if (!referencePath.isDirectory()) { listener.getLogger() - .println( - "[WARNING] Reference path does not contain an objects directory (no git repo?): " - + objectsPath); + .println("[WARNING] Reference path is not a directory: " + reference); } else { - try { - File alternates = new File(workspace, ".git/objects/info/alternates"); - String absoluteReference = - objectsPath.getAbsolutePath().replace('\\', '/'); - listener.getLogger().println("Using reference repository: " + reference); - // git implementations on windows also use - try (PrintWriter w = new PrintWriter(alternates, StandardCharsets.UTF_8)) { - // git implementations on windows also use - w.print(absoluteReference); + File objectsPath = getObjectsFile(referencePath); + if (objectsPath == null || !objectsPath.isDirectory()) { + listener.getLogger() + .println("[WARNING] Reference path does not contain an objects directory " + + "(no git repo?): " + objectsPath); + } else { + // Go behind git's back to write a meta file in new workspace + try { + File alternates = new File(workspace, ".git/objects/info/alternates"); + String absoluteReference = + objectsPath.getAbsolutePath().replace('\\', '/'); + listener.getLogger().println("Using reference repository: " + reference); + try (PrintWriter w = new PrintWriter(alternates, StandardCharsets.UTF_8)) { + // git implementations on windows also use + w.print(absoluteReference); + } + } catch (FileNotFoundException e) { + listener.error("Failed to setup reference"); } - } catch (FileNotFoundException e) { - listener.error("Failed to setup reference"); } } } diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/LegacyCompatibleGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/LegacyCompatibleGitAPIImpl.java index 3d6b46d049..5f2c4617fe 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/LegacyCompatibleGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/LegacyCompatibleGitAPIImpl.java @@ -3,6 +3,7 @@ import static java.util.Arrays.copyOfRange; import static org.apache.commons.lang.StringUtils.join; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.model.TaskListener; import hudson.plugins.git.GitException; import hudson.plugins.git.IGitAPI; @@ -10,12 +11,22 @@ import hudson.plugins.git.Revision; import hudson.plugins.git.Tag; import hudson.remoting.Channel; +import java.io.BufferedReader; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStreamReader; import java.net.URISyntaxException; +import java.nio.file.Paths; +import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.apache.commons.codec.digest.DigestUtils; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; @@ -35,6 +46,7 @@ */ abstract class LegacyCompatibleGitAPIImpl extends AbstractGitAPIImpl implements IGitAPI { + private static final Logger LOGGER = Logger.getLogger(LegacyCompatibleGitAPIImpl.class.getName()); private HostKeyVerifierFactory hostKeyFactory; /** @@ -181,6 +193,1052 @@ public void clone(RemoteConfig rc, boolean useShallowClone) throws GitException, clone(source, rc.getName(), useShallowClone, null); } + /** For referenced directory check if it is a full or bare git repo + * and return the File object for its "objects" sub-directory. + * (Note that for submodules and other cases with externalized Git + * metadata, the "objects" directory may be NOT under "reference"). + * If there is nothing to find, or inputs are bad, returns null. + * The idea is that checking for null allows to rule out non-git + * paths, while a not-null return value is instantly usable by + * some code which plays with git under its hood. + */ + public static File getObjectsFile(String reference) { + if (reference == null || reference.isEmpty()) { + return null; + } + return getObjectsFile(new File(reference)); + } + + public static File getObjectsFile(File reference) { + // reference pathname can either point to a "normal" workspace + // checkout or a bare repository + + if (reference == null) { + return reference; + } + + if (!reference.exists()) { + return null; + } + + if (!reference.isDirectory()) { + return null; + } + + File fGit = new File(reference, ".git"); // workspace - file, dir or symlink to those + File objects = null; + + if (fGit.exists()) { + if (fGit.isDirectory()) { + objects = new File(fGit, "objects"); // this might not exist or not be a dir - checked below + /* + if (objects == null) { // spotbugs dislikes this, since "new File()" should not return null + return objects; // Some Java error, could not make object from the paths involved + } + */ + LOGGER.log( + Level.FINEST, + "getObjectsFile(): found an fGit '" + fGit.getAbsolutePath() + "' which is a directory"); + } else { + // If ".git" FS object exists and is a not-empty file (and + // is not a dir), then its content may point to some other + // filesystem location for the Git-internal data. + // For example, a checked-out submodule workspace can point + // to the index and other metadata stored in its "parent" + // repository's directory: + // "gitdir: ../.git/modules/childRepoName" + LOGGER.log( + Level.FINEST, + "getObjectsFile(): found an fGit '" + fGit.getAbsolutePath() + "' which is NOT a directory"); + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(new FileInputStream(fGit), "UTF-8"))) { + String line; + while ((line = reader.readLine()) != null) { + String[] parts = line.split(":", 2); + if (parts.length >= 2) { + String key = parts[0].trim(); + String value = parts[1].trim(); + if (key.equals("gitdir")) { + objects = new File(reference, value); + LOGGER.log( + Level.FINE, + "getObjectsFile(): while looking for 'gitdir:' in '" + + fGit.getAbsolutePath() + + "', found reference to objects which should be at: '" + + objects.getAbsolutePath() + "'"); + // Note: we don't use getCanonicalPath() here to avoid further filesystem + // access and possible exceptions (the getAbsolutePath() is about string + // processing), but callers might benefit from canonicising and ensuring + // unique pathnames (for equality checks etc.) with relative components + // and symlinks resolved. + // On another hand, keeping the absolute paths, possibly, relative to a + // parent directory as prefix, allows callers to match/compare such parent + // prefixes for the contexts the callers would define for themselves. + break; + } + LOGGER.log( + Level.FINEST, + "getObjectsFile(): while looking for 'gitdir:' in '" + fGit.getAbsolutePath() + + "', ignoring line: " + line); + } + } + if (objects == null) { + LOGGER.log( + Level.WARNING, + "getObjectsFile(): failed to parse '" + fGit.getAbsolutePath() + + "': did not contain a 'gitdir:' entry"); + } + } catch (IOException e) { + LOGGER.log( + Level.SEVERE, + "getObjectsFile(): failed to parse '" + fGit.getAbsolutePath() + "': " + e.toString()); + objects = null; + } + } + } else { + LOGGER.log(Level.FINEST, "getObjectsFile(): did not find any checked-out '" + fGit.getAbsolutePath() + "'"); + } + + if (objects == null || !objects.isDirectory()) { + // either reference path is bare repo (no ".git" inside), + // or we have failed interpreting ".git" contents above + objects = new File(reference, "objects"); // bare + /* + if (objects == null) { + return objects; // Some Java error, could not make object from the paths involved + } + */ + // This clause below is redundant for production, but useful for troubleshooting + if (objects.exists()) { + if (objects.isDirectory()) { + LOGGER.log( + Level.FINEST, + "getObjectsFile(): found a bare '" + objects.getAbsolutePath() + "' which is a directory"); + } else { + LOGGER.log( + Level.FINEST, + "getObjectsFile(): found a bare '" + objects.getAbsolutePath() + + "' which is NOT a directory"); + } + } else { + LOGGER.log(Level.FINEST, "getObjectsFile(): did not find any bare '" + objects.getAbsolutePath() + "'"); + } + } + + if (!objects.exists()) { + return null; + } + + if (!objects.isDirectory()) { + return null; + } + + // If we get here, we have a non-null File referencing a + // "(.git/)objects" subdir under original referencePath + return objects; + } + + /** Handle magic strings in the reference pathname to sort out patterns + * classified as evaluated by parametrization, as handled below + * + * @param reference Pathname (maybe with magic suffix) to reference repo + */ + public static Boolean isParameterizedReferenceRepository(File reference) { + if (reference == null) { + return false; + } + return isParameterizedReferenceRepository(reference.getPath()); + } + + public static Boolean isParameterizedReferenceRepository(String reference) { + if (reference == null || reference.isEmpty()) { + return false; + } + + if (reference.endsWith("/${GIT_URL_SHA256}")) { + return true; + } + + if (reference.endsWith("/${GIT_URL_SHA256_FALLBACK}")) { + return true; + } + + if (reference.endsWith("/${GIT_URL_BASENAME}")) { + return true; + } + + if (reference.endsWith("/${GIT_URL_BASENAME_FALLBACK}")) { + return true; + } + + if (reference.endsWith("/${GIT_SUBMODULES}")) { + return true; + } + + if (reference.endsWith("/${GIT_SUBMODULES_FALLBACK}")) { + return true; + } + + return false; + } + + /** There are many ways to spell an URL to the same repository even if + * using the same access protocol. This routine converts the "url" string + * in a way that helps us confirm whether two spellings mean same thing. + */ + @SuppressFBWarnings( + value = "DMI_HARDCODED_ABSOLUTE_FILENAME", + justification = "Path operations below intentionally use absolute '/' in some cases") + public static String normalizeGitUrl(String url, Boolean checkLocalPaths) { + String urlNormalized = url.replaceAll("/*$", "").replaceAll(".git$", "").toLowerCase(Locale.ENGLISH); + if (!url.contains("://")) { + if (!url.startsWith("/") && !url.startsWith(".")) { + // Not an URL with schema, not an absolute or relative pathname + if (checkLocalPaths) { + File urlPath = new File(url); + if (urlPath.exists()) { + try { + // Check if the string in urlNormalized is a valid + // relative path (subdir) in current working directory + urlNormalized = "file://" + + Paths.get(Paths.get("").toAbsolutePath().toString() + "/" + urlNormalized) + .normalize() + .toString(); + } catch (java.nio.file.InvalidPathException ipe1) { + // e.g. Illegal char <:> at index 30: + // C:\jenkins\git-client-plugin/c:\jenkins\git-client-plugin\target\clone + try { + // Re-check in another manner + urlNormalized = "file://" + + Paths.get(Paths.get("", urlNormalized) + .toAbsolutePath() + .toString()) + .normalize() + .toString(); + } catch (java.nio.file.InvalidPathException ipe2) { + // Finally, fall back to checking the originally + // fully-qualified path + urlNormalized = "file://" + + Paths.get(Paths.get("/", urlNormalized) + .toAbsolutePath() + .toString()) + .normalize() + .toString(); + } + } + } else { + // Also not a subdirectory of current dir without "./" prefix... + urlNormalized = "ssh://" + urlNormalized; + } + } else { + // Assume it is not a path + urlNormalized = "ssh://" + urlNormalized; + } + } else { + // Looks like a local path + if (url.startsWith("/")) { + urlNormalized = "file://" + urlNormalized; + } else { + urlNormalized = "file://" + + Paths.get(Paths.get("").toAbsolutePath().toString() + "/" + urlNormalized) + .normalize() + .toString(); + } + } + } + + LOGGER.log( + Level.FINEST, "normalizeGitUrl('" + url + "', " + checkLocalPaths.toString() + ") => " + urlNormalized); + return urlNormalized; + } + + /** Find referenced URLs in this repo and its submodules (or other + * subdirs with git repos), recursively. Current primary use is for + * parameterized refrepo/${GIT_SUBMODULES} handling. + * + * @return an AbstractMap.SimpleEntry, containing a Boolean to denote + * an exact match (or lack thereof) for the needle (if searched for), + * and a Set of (unique) String arrays, representing: + * [0] directory of nested submodule (relative to current workspace root) + * The current workspace would be listed as directory "" and consumers + * should check these entries last if they care for most-specific hits + * with smaller-scope reference repositories. + * [1] url as returned by getRemoteUrls() - fetch URLs, maybe several + * entries per remote + * [2] urlNormalized from normalizeGitUrl(url, true) (local pathnames + * fully qualified) + * [3] remoteName as defined in that nested submodule + * + * If the returned SimpleEntry has the Boolean flag as False but also + * a Set which is not empty, and a search for "needle" was requested, + * then that Set lists some not-exact matches for existing sub-dirs + * with repositories that seem likely to be close hits (e.g. remotes + * there *probably* point to other URLs of same repo as the needle, + * or its forks - so these directories are more likely than others to + * contain the reference commits needed for the faster git checkouts). + * + * For a search with needle==null, the Boolean flag would be False too, + * and the Set would just detail all found sub-repositories. + * + * @param referenceBaseDir - the reference repository, or container thereof + * @param needle - an URL which (or its normalized variant coming from + * normalizeGitUrl(url, true)) we want to find: + * if it is not null - then stop and return just hits + * for it as soon as we have something. + * @param checkRemotesInReferenceBaseDir - if true (reasonable default for + * external callers), the referenceBaseDir would be added + * to the list of dirs for listing known remotes in search + * for a needle match or for the big listing. Set to false + * when recursing, since this directory was checked already + * as part of parent directory inspection. + */ + public SimpleEntry> getSubmodulesUrls( + String referenceBaseDir, String needle, Boolean checkRemotesInReferenceBaseDir) { + // Keep track of where we've already looked in the "result" Set, to + // avoid looking in same places (different strategies below) twice. + // And eventually return this Set or part of it as the answer. + LinkedHashSet result = new LinkedHashSet<>(); // Retain order of insertion + File f = null; + // Helper list storage in loops below + ArrayList arrDirnames = new ArrayList(); + + // Note: an absolute path is not necessarily the canonical one + // We want to hit same dirs only once, so canonicize paths below + String referenceBaseDirAbs; + try { + referenceBaseDirAbs = new File(referenceBaseDir).getAbsoluteFile().getCanonicalPath(); + } catch (IOException e) { + // Access error while dereferencing some parent?.. + referenceBaseDirAbs = new File(referenceBaseDir).getAbsoluteFile().toString(); + LOGGER.log( + Level.SEVERE, + "getSubmodulesUrls(): failed to canonicize '" + + referenceBaseDir + "' => '" + referenceBaseDirAbs + "': " + + e.toString()); + // return new SimpleEntry<>(false, result); + } + + // "this" during a checkout typically represents the job workspace, + // but we want to inspect the reference repository located elsewhere + // with the same implementation as the end-user set up (CliGit/jGit) + GitClient referenceGit = this.newGit(referenceBaseDirAbs); + + Boolean isBare = false; + try { + isBare = ((hudson.plugins.git.IGitAPI) referenceGit).isBareRepository(); + } catch (InterruptedException | GitException e) { + // Proposed base directory whose subdirs contain refrepos might + // itself be not a repo. Shouldn't be per reference scripts, but... + if (e.toString().contains("GIT_DISCOVERY_ACROSS_FILESYSTEM")) { + // Note the message may be localized, envvar name should not be: + // stderr: fatal: not a git repository (or any parent up to mount point /some/path) + // Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set). + // As far as the logic below is currently concerned, we do not + // look for submodules directly in a bare repo. + isBare = true; + } else { + // Some other error + isBare = false; // At least try to look into submodules... + isBare = false; + } + + LOGGER.log( + Level.SEVERE, + "getSubmodulesUrls(): failed to determine isBareRepository() in '" + + referenceBaseDirAbs + "'; " + "assuming '" + isBare + "': " + + e.toString()); + } + + // Simplify checks below by stating a useless needle is null + if (needle != null && needle.isEmpty()) { + needle = null; + } + // This is only used and populated if needle is not null + String needleNorm = null; + + // This is only used and populated if needle is not null, and + // can be used in the end to filter not-exact match suggestions + String needleBasename = null; + String needleBasenameLC = null; + String needleNormBasename = null; + String needleSha = null; + + // If needle is not null, first look perhaps in the subdir(s) named + // with base-name of the URL with and without a ".git" suffix, then + // in SHA256 named dir that can match it; note that this use-case + // might pan out also if "this" repo is bare and can not have "proper" + // git submodules - but was prepared for our other options. + if (needle != null) { + int sep = needle.lastIndexOf("/"); + if (sep < 0) { + needleBasename = needle; + } else { + needleBasename = needle.substring(sep + 1); + } + needleBasename = needleBasename.replaceAll(".[Gg][Ii][Tt]$", ""); + + needleNorm = normalizeGitUrl(needle, true); + sep = needleNorm.lastIndexOf("/"); + if (sep < 0) { + needleNormBasename = needleNorm; + } else { + needleNormBasename = needleNorm.substring(sep + 1); + } + needleNormBasename = needleNormBasename.replaceAll(".git$", ""); + + // Try with the basename without .git extension, and then with one. + // First we try the caller-provided string casing, then normalized. + // Note that only after this first smaller pass which we hope to + // succeed quickly, we engage in heavier (by I/O and computation) + // investigation of submodules, and then similar loop against any + // remaining direct subdirs that contain a ".git" (or "objects") + // FS object. + arrDirnames.add(referenceBaseDirAbs + "/" + needleBasename); + arrDirnames.add(referenceBaseDirAbs + "/" + needleBasename + ".git"); + needleBasenameLC = needleBasename.toLowerCase(Locale.ENGLISH); + if (!needleBasenameLC.equals(needleBasename)) { + // Retry with lowercased dirname + arrDirnames.add(referenceBaseDirAbs + "/" + needleBasenameLC); + arrDirnames.add(referenceBaseDirAbs + "/" + needleBasenameLC + ".git"); + } + if (!needleNormBasename.equals(needleBasenameLC)) { + arrDirnames.add(referenceBaseDirAbs + "/" + needleNormBasename); + arrDirnames.add(referenceBaseDirAbs + "/" + needleNormBasename + ".git"); + } + + needleSha = DigestUtils.sha256Hex(needleNorm); + arrDirnames.add(referenceBaseDirAbs + "/" + needleSha); + arrDirnames.add(referenceBaseDirAbs + "/" + needleSha + ".git"); + + LOGGER.log( + Level.FINE, + "getSubmodulesUrls(): looking at basename-like subdirs under base refrepo '" + referenceBaseDirAbs + + "', per arrDirnames: " + arrDirnames.toString()); + + for (String dirname : arrDirnames) { + f = new File(dirname); + LOGGER.log( + Level.FINEST, + "getSubmodulesUrls(): probing dir at abs pathname '" + dirname + "' if it exists"); + if (getObjectsFile(f) != null) { + try { + LOGGER.log( + Level.FINE, + "getSubmodulesUrls(): looking for submodule URL needle='" + needle + + "' in existing refrepo subdir '" + dirname + "'"); + GitClient g = referenceGit.subGit(dirname); + LOGGER.log( + Level.FINE, + "getSubmodulesUrls(): checking git workspace in dir '" + + g.getWorkTree().absolutize().toString() + "'"); + Map uriNames = g.getRemoteUrls(); + LOGGER.log( + Level.FINEST, + "getSubmodulesUrls(): sub-git getRemoteUrls() returned this Map uriNames: " + + uriNames.toString()); + for (Map.Entry pair : uriNames.entrySet()) { + String remoteName = pair.getValue(); // whatever the git workspace config calls it + String uri = pair.getKey(); + String uriNorm = normalizeGitUrl(uri, true); + LOGGER.log( + Level.FINE, + "getSubmodulesUrls(): checking uri='" + uri + "' (uriNorm='" + uriNorm + + "') vs needle"); + if (needleNorm.equals(uriNorm) || needle.equals(uri)) { + result = new LinkedHashSet<>(); + result.add(new String[] {dirname, uri, uriNorm, remoteName}); + return new SimpleEntry<>(true, result); + } + // Cache the finding to avoid the dirname later, if we + // get to that; but no checks are needed in this loop + // which by construct looks at different dirs so far. + result.add(new String[] {dirname, uri, uriNorm, remoteName}); + } + } catch (Throwable t) { + // ignore, go to next slide + LOGGER.log( + Level.FINE, + "getSubmodulesUrls(): probing dir '" + dirname + + "' resulted in an exception or error (will go to next item):\n" + + t.toString()); + } + } + } + } // if needle, look in basename-like and SHA dirs first + + // Needle or not, the rest of directory walk to collect data is the + // same, so follow a list of whom we want to visit in likely-quickest + // hit order. Note that if needle is null, we walk over everything + // that makes sense to visit, to return info on all git remotes found + // in or under this directory; however if it is not-null, we still + // try to have minimal overhead to complete as soon as we match it. + // TODO: Refactor to avoid lookups of dirs that may prove not needed + // in the end (aim for less I/Os to find the goal)... or is one dir + // listing a small price to pay for maintaining one unified logic? + + // If current dir does have submodules, first dig into submodules, + // when there is no deeper to drill, report remote URLs and step + // back from recursion. This way we have least specific repo last, + // if several have the replica (assuming the first hits are smaller + // scopes). + + // Track where we have looked already; note that values in result[] + // (if any from needle-search above) are absolute pathnames + LinkedHashSet checkedDirs = new LinkedHashSet<>(); + for (String[] resultEntry : result) { + checkedDirs.add(resultEntry[0]); + } + + /* + // TBD: Needs a way to list submodules in given workspace and convert + // that into (relative) subdirs, possibly buried some levels deep, for + // cases where the submodule is defined in parent with the needle URL. + // Maybe merge with current "if isBare" below, to optionally seed + // same arrDirnames with different values and check remotes listed + // in those repos. + // If current repo *is NOT* bare - check its submodules + // (the .gitmodules => submodule.MODNAME.{url,path} mapping) + // but this essentially does not look into any subdirectory. + // But we can add at higher priority submodule path(s) whose + // basename of the URL matches the needleBasename. And then + // other submodule paths to inspect before arbitrary subdirs. + if (!isBare) { + try { + // For each current workspace (recurse or big loop in same context?): + // public GitClient subGit(String subdir) => would this.subGit(...) + // give us a copy of this applied class instance (CLI Git vs jGit)? + // get submodule name-vs-one-url from .gitmodules if present, for a + // faster possible answer (only bother if needle is not null?) + // try { getSubmodules("HEAD") ... } => List filtered for + // "commit" items + // * if we are recursed into a "leaf" project and inspect ITS + // submodules, look at all git tips or even commits, to find + // and inspect all unique (by hash) .gitmodule objects, since + // over time or in different branches a "leaf" project could + // reference different subs? + // getRemoteUrls() => Map + //// arrDirnames.clear(); + + // TODO: Check subdirs that are git workspaces, and remove "|| true" above + //// LinkedHashSet checkedDirs = new LinkedHashSet<>(); + //// for (String[] resultEntry : result) { + //// checkedDirs.add(resultEntry[0]); + //// } + + LOGGER.log(Level.FINE, "getSubmodulesUrls(): looking for submodule URL needle='" + needle + "' in submodules of refrepo, if any"); + Map uriNames = referenceGit.getRemoteUrls(); + for (Map.Entry pair : uriNames.entrySet()) { + String uri = pair.getKey(); + String uriNorm = normalizeGitUrl(uri, true); + LOGGER.log(Level.FINE, "getSubmodulesUrls(): checking uri='" + uri + "' (uriNorm='" + uriNorm + "')"); + LOGGER.log(Level.FINEST, "getSubmodulesUrls(): sub-git getRemoteUrls() returned this Map: " + uriNames.toString()); + if (needleNorm.equals(uriNorm) || needle.equals(uri)) { + result = new LinkedHashSet<>(); + result.add(new String[]{fAbs, uri, uriNorm, pair.getValue()}); + return result; + } + // Cache the finding to avoid the dirname later, if we + // get to that; but no checks are needed in this loop + // which by construct looks at different dirs so far. + result.add(new String[]{fAbs, uri, uriNorm, pair.getValue()}); + } + } catch (Exception e) { + // ignore, go to next slide + } + } + */ + + // If current repo *is* bare (can't have proper submodules), or if the + // end-users just cloned or linked some more repos into this container, + // follow up with direct child dirs that have a ".git" (or "objects") + // FS object inside: + + // Check subdirs that are git workspaces; note that values in checkedDirs + // are absolute pathnames. If we did look for the needle, array already + // starts with some "prioritized" pathnames which we should not directly + // inspect again... but should recurse into first anyhow. + File[] directories = new File(referenceBaseDirAbs).listFiles(File::isDirectory); + if (directories != null) { + // listFiles() "...returns null if this abstract pathname + // does not denote a directory, or if an I/O error occurs" + for (File dir : directories) { + if (getObjectsFile(dir) != null) { + String dirname = dir.getPath().replaceAll("/*$", ""); + if (!checkedDirs.contains(dirname)) { + arrDirnames.add(dirname); + } + } + } + } + + // Finally check pattern's parent dir + // * Look at remote URLs in current dir after the guessed subdirs failed, + // and return then. + if (checkRemotesInReferenceBaseDir) { + if (getObjectsFile(referenceBaseDirAbs) != null) { + arrDirnames.add(referenceBaseDirAbs); + } + } + + LOGGER.log( + Level.FINE, + "getSubmodulesUrls(): looking at " + + ((isBare ? "" : "submodules first, then ")) + + "all subdirs that have a .git, under refrepo '" + + referenceBaseDirAbs + "' per absolute arrDirnames: " + + arrDirnames.toString()); + + for (String dirname : arrDirnames) { + // Note that here dirnames deal in absolutes + f = new File(dirname); + LOGGER.log(Level.FINEST, "getSubmodulesUrls(): probing dir '" + dirname + "' if it exists"); + if (f.exists() && f.isDirectory()) { + // No checks for ".git" or "objects" this time, already checked above + // by getObjectsFile(). Probably should not check exists/dir either, + // but better be on the safe side :) + if (!checkedDirs.contains(dirname)) { + try { + LOGGER.log( + Level.FINE, + "getSubmodulesUrls(): looking " + + ((needle == null) ? "" : "for submodule URL needle='" + needle + "' ") + + "in existing refrepo dir '" + dirname + "'"); + GitClient g = this.newGit(dirname); + LOGGER.log( + Level.FINE, + "getSubmodulesUrls(): checking git workspace in dir '" + + g.getWorkTree().absolutize().toString() + "'"); + Map uriNames = g.getRemoteUrls(); + LOGGER.log( + Level.FINEST, + "getSubmodulesUrls(): sub-git getRemoteUrls() returned this Map uriNames: " + + uriNames.toString()); + for (Map.Entry pair : uriNames.entrySet()) { + String remoteName = pair.getValue(); // whatever the git workspace config calls it + String uri = pair.getKey(); + String uriNorm = normalizeGitUrl(uri, true); + LOGGER.log( + Level.FINE, + "getSubmodulesUrls(): checking uri='" + uri + "' (uriNorm='" + uriNorm + + "') vs needle"); + if (needle != null + && needleNorm != null + && (needleNorm.equals(uriNorm) || needle.equals(uri))) { + result = new LinkedHashSet<>(); + result.add(new String[] {dirname, uri, uriNorm, remoteName}); + return new SimpleEntry<>(true, result); + } + // Cache the finding to return eventually, for each remote: + // * absolute dirname of a Git workspace + // * original remote URI from that workspace's config + // * normalized remote URI + // * name of the remote from that workspace's config ("origin" etc) + result.add(new String[] {dirname, uri, uriNorm, remoteName}); + } + } catch (Throwable t) { + // ignore, go to next slide + LOGGER.log( + Level.FINE, + "getSubmodulesUrls(): probing dir '" + dirname + + "' resulted in an exception or error (will go to next item):\n" + + t.toString()); + } + } + + // Here is a good spot to recurse this routine into a + // subdir that is already a known git workspace, to + // add its data to list and/or return a found needle. + LOGGER.log(Level.FINE, "getSubmodulesUrls(): recursing into dir '" + dirname + "'..."); + SimpleEntry> subEntriesRet = getSubmodulesUrls(dirname, needle, false); + Boolean subEntriesExactMatched = subEntriesRet.getKey(); + LinkedHashSet subEntries = subEntriesRet.getValue(); + LOGGER.log( + Level.FINE, + "getSubmodulesUrls(): returned from recursing into dir '" + dirname + "' with " + + subEntries.size() + " found mappings"); + if (!subEntries.isEmpty()) { + if (needle != null && subEntriesExactMatched) { + // We found nothing... until now! Bubble it up! + LOGGER.log( + Level.FINE, + "getSubmodulesUrls(): got an exact needle match from recursing into dir '" + dirname + + "': " + subEntries.iterator().next()[0]); + return subEntriesRet; + } + // ...else collect results to inspect and/or propagate later + result.addAll(subEntries); + } + } + } + + // Nothing found, if we had a needle - so report there are no hits + // If we did not have a needle, we did not search for it - return + // below whatever result we have, then. + if (needle != null) { + if (result.size() == 0) { + // Completely nothing git-like found here, return quickly + return new SimpleEntry<>(false, result); + } + + // Handle suggestions (not-exact matches) if something from + // results looks like it is related to the needle. + LinkedHashSet resultFiltered = new LinkedHashSet<>(); + + /* + if (!checkRemotesInReferenceBaseDir) { + // Overload the flag's meaning to only parse results once, + // in the parent dir? + return new SimpleEntry<>(false, resultFiltered); + } + */ + + // Separate lists by suggestion priority: + // 1) URI basename similarity + // 2) Directory basename similarity + LinkedHashSet resultFiltered1 = new LinkedHashSet<>(); + LinkedHashSet resultFiltered2 = new LinkedHashSet<>(); + + LinkedHashSet suggestedDirs = new LinkedHashSet<>(); + for (String[] resultEntry : result) { + checkedDirs.add(resultEntry[0]); + } + + for (String[] subEntry : result) { + // Iterating to filter suggestions in order of original + // directory-walk prioritization under current reference + String dirName = subEntry[0]; + String uriNorm = subEntry[2]; + Integer sep; + String uriNormBasename; + String dirBasename; + + // Match basename of needle vs. a remote tracked by an + // existing git directory (automation-ready normalized URL) + sep = uriNorm.lastIndexOf("/"); + if (sep < 0) { + uriNormBasename = uriNorm; + } else { + uriNormBasename = uriNorm.substring(sep + 1); + } + uriNormBasename = uriNormBasename.replaceAll(".git$", ""); + + if (uriNormBasename.equals(needleNormBasename)) { + resultFiltered1.add(subEntry); + } + + if (!suggestedDirs.contains(dirName)) { + // Here just match basename of needle vs. an existing + // sub-git directory base name + suggestedDirs.add(dirName); + + sep = dirName.lastIndexOf("/"); + if (sep < 0) { + dirBasename = dirName; + } else { + dirBasename = dirName.substring(sep + 1); + } + dirBasename = dirBasename.replaceAll(".git$", ""); + + if (dirBasename.equals(needleNormBasename)) { + resultFiltered2.add(subEntry); + } + } + } + + // Concatenate suggestions in order of priority. + // Hopefully the Set should deduplicate entries + // if something matched twice :) + resultFiltered.addAll(resultFiltered1); // URLs + resultFiltered.addAll(resultFiltered2); // Dirnames + + // Note: flag is false since matches (if any) are + // not exactly for the Git URL requested by caller + return new SimpleEntry<>(false, resultFiltered); + } + + // Did not look for anything in particular + return new SimpleEntry<>(false, result); + } + + /** See above. With null needle, returns all data we can find under the + * referenceBaseDir tree (can take a while) for the caller to parse */ + public SimpleEntry> getSubmodulesUrls(String referenceBaseDir) { + return getSubmodulesUrls(referenceBaseDir, null, true); + } + /* Do we need a completely parameter-less variant to look under current + * work dir aka Paths.get("").toAbsolutePath().toString(), or under "this" + * GitClient workspace ?.. */ + + /** Yield the File object for the reference repository local filesystem + * pathname. Note that the provided string may be suffixed with expandable + * tokens which allow to store a filesystem structure of multiple small + * reference repositories instead of a big combined repository, while + * providing a single inheritable configuration string value. Callers + * can check whether the original path was used or mangled into another + * by comparing their "reference" with returned object's File.getName(). + * + * At some point this plugin might also maintain that filesystem structure. + * + * @param reference Pathname (maybe with magic suffix) to reference repo + * @param url URL of the repository being cloned, to help choose a + * suitable parameterized reference repo subdirectory. + */ + public File findParameterizedReferenceRepository(File reference, String url) { + if (reference == null) { + return reference; + } + return findParameterizedReferenceRepository(reference.getPath(), url); + } + + public File findParameterizedReferenceRepository(String reference, String url) { + if (reference == null || reference.isEmpty()) { + return null; + } + + File referencePath = new File(reference); + // For mass-configured jobs, like Organization Folders, which inherit + // a refrepo setting (String) into generated MultiBranch Pipelines and + // leaf jobs made for each repo branch and PR, the code below allows + // us to support parameterized paths, with one string leading to many + // reference repositories fanned out under a common location. + // This also works for legacy jobs using a Git SCM. + + // TODO: Consider a config option whether to populate absent reference + // repos (If the expanded path does not have git repo data right now, + // should we populate it into the location expanded by logic below), + // or update existing ones before pulling commits, and how to achieve + // that. Currently this is something that comments elsewhere in the + // git-client-plugin and/or articles on reference repository setup + // considered to be explicitly out of scope of the plugin. + // Note that this pre-population (or update) is done by caller with + // their implementation of git and site-specific connectivity and + // storage desigh, e.g. in case of a build farm it is more likely + // to be a shared path from a common storage server only readable to + // Jenkins and its agents, so write-operations would be done by helper + // scripts that log into the shared storage server to populate or + // update the reference repositories. Note that users may also + // want to run their own scripts to "populate" reference repos + // as symlinks to existing other repos, to support combined + // repo setup for different URLs pointing to same upstream, + // or storing multiple closely related forks together. + // This feature was developed along with a shell script to manage + // reference repositories, both in original combined-monolith layout, + // and in the subdirectory fanout compatible with plugin code below: + // https://github.com/jimklimov/git-scripts/blob/master/register-git-cache.sh + + // Note: Initially we expect the reference to be a realistic dirname + // with a special suffix to substitute after the logic below, so the + // referencePath for that verbatim funny string should not exist now: + if (!referencePath.exists() && isParameterizedReferenceRepository(reference) && url != null && !url.isEmpty()) { + // Drop the trailing keyword to know the root refrepo dirname + String referenceBaseDir = reference.replaceAll("/\\$\\{GIT_[^\\}]*\\}$", ""); + + File referenceBaseDirFile = new File(referenceBaseDir); + if (!referenceBaseDirFile.exists()) { + LOGGER.log(Level.WARNING, "Base Git reference directory " + referenceBaseDir + " does not exist"); + return null; + } + if (!referenceBaseDirFile.isDirectory()) { + LOGGER.log(Level.WARNING, "Base Git reference directory " + referenceBaseDir + " is not a directory"); + return null; + } + + // Note: this normalization might crush several URLs into one, + // and as far as is known this would be the goal - people tend + // to use or omit .git suffix, or spell with varied case, while + // it means the same thing to known Git platforms, except local + // dirs on case-sensitive filesystems. + // The actual reference repository directory may choose to have + // original URL strings added as remotes (in case some are case + // sensitive and different). + String urlNormalized = normalizeGitUrl(url, true); + + // Note: currently unit-tests expect this markup on stderr: + System.err.println("reference='" + reference + "'\n" + + "url='" + url + "'\n" + + "urlNormalized='" + urlNormalized + "'\n"); + + // Let users know why there are many "git config --list" lines in their build log: + LOGGER.log( + Level.INFO, + "Trying to resolve parameterized Git reference repository '" + reference + + "' into a specific (sub-)directory to use for URL '" + url + "' ..."); + + String referenceExpanded = null; + if (reference.endsWith("/${GIT_URL_SHA256}")) { + // This may be the more portable solution with regard to filesystems + referenceExpanded = + reference.replaceAll("\\$\\{GIT_URL_SHA256\\}$", DigestUtils.sha256Hex(urlNormalized)); + } else if (reference.endsWith("/${GIT_URL_SHA256_FALLBACK}")) { + // The safest option - fall back to parent directory if + // the expanded one does not have git repo data right now: + // it allows to gradually convert a big combined reference + // repository into smaller chunks without breaking builds. + referenceExpanded = + reference.replaceAll("\\$\\{GIT_URL_SHA256_FALLBACK\\}$", DigestUtils.sha256Hex(urlNormalized)); + if (getObjectsFile(referenceExpanded) == null && getObjectsFile(referenceExpanded + ".git") == null) { + // chop it off, use main directory + referenceExpanded = referenceBaseDir; + } + } else if (reference.endsWith("/${GIT_URL_BASENAME}") + || reference.endsWith("/${GIT_URL_BASENAME_FALLBACK}")) { + // This may be the more portable solution with regard to filesystems + // First try with original user-provided casing of the URL (if local + // dirs were cloned manually) + int sep = url.lastIndexOf("/"); + String needleBasename; + if (sep < 0) { + needleBasename = url; + } else { + needleBasename = url.substring(sep + 1); + } + needleBasename = needleBasename.replaceAll(".git$", ""); + + if (reference.endsWith("/${GIT_URL_BASENAME}")) { + referenceExpanded = reference.replaceAll("\\$\\{GIT_URL_BASENAME\\}$", needleBasename); + } else { // if (reference.endsWith("/${GIT_URL_BASENAME_FALLBACK}")) { + referenceExpanded = reference.replaceAll("\\$\\{GIT_URL_BASENAME_FALLBACK\\}$", needleBasename); + if (url.equals(urlNormalized) + && getObjectsFile(referenceExpanded) == null + && getObjectsFile(referenceExpanded + ".git") == null) { + // chop it off, use main directory (only if we do not check urlNormalized separately below) + referenceExpanded = referenceBaseDir; + } + } + + if (!url.equals(urlNormalized) + && getObjectsFile(referenceExpanded) == null + && getObjectsFile(referenceExpanded + ".git") == null) { + // Retry with automation-ready normalized URL + sep = urlNormalized.lastIndexOf("/"); + if (sep < 0) { + needleBasename = urlNormalized; + } else { + needleBasename = urlNormalized.substring(sep + 1); + } + needleBasename = needleBasename.replaceAll(".git$", ""); + + if (reference.endsWith("/${GIT_URL_BASENAME}")) { + referenceExpanded = reference.replaceAll("\\$\\{GIT_URL_BASENAME\\}$", needleBasename); + } else { // if (reference.endsWith("/${GIT_URL_BASENAME_FALLBACK}")) { + referenceExpanded = reference.replaceAll("\\$\\{GIT_URL_BASENAME_FALLBACK\\}$", needleBasename); + if (getObjectsFile(referenceExpanded) == null + && getObjectsFile(referenceExpanded + ".git") == null) { + // chop it off, use main directory + referenceExpanded = referenceBaseDir; + } + } + } + } else if (reference.endsWith("/${GIT_SUBMODULES}") || reference.endsWith("/${GIT_SUBMODULES_FALLBACK}")) { + // Here we employ git submodules - so we can reliably match + // remote URLs (easily multiple) to particular modules, and + // yet keep separate git index directories per module with + // smaller scopes - much quicker to check out from than one + // huge combined repo. It would also be much more native to + // tools and custom scriptware that can be involved. + // Beside git-submodule parsing (that only points to one URL + // at a time) his also covers a search for subdirectories + // that host a git repository whose remotes match the URL, + // to handle co-hosting of several remotes (different URLs + // to same repository, e.g. SSH and HTTPS; mirrors; forks). + + // Assuming the provided "reference" directory already hosts + // submodules, we use git tools to find the one subdir which + // has a registered remote URL equivalent (per normalization) + // to the provided "url". + + // Note: we pass the unmodified "url" value here, the routine + // differentiates original spelling vs. normalization while + // looking for its needle in the haystack. + SimpleEntry> subEntriesRet = + getSubmodulesUrls(referenceBaseDir, url, true); + Boolean subEntriesExactMatched = subEntriesRet.getKey(); + LinkedHashSet subEntries = subEntriesRet.getValue(); + if (!subEntries.isEmpty()) { + // Normally we should only have one entry here, as sorted + // by the routine, and prefer that first option if a new + // reference repo would have to be made (and none exists). + // If several entries are present after all, iterate until + // first existing hit and return the first entry otherwise. + if (!subEntriesExactMatched) { // else look at first entry below + for (String[] subEntry : subEntries) { + if (getObjectsFile(subEntry[0]) != null || getObjectsFile(subEntry[0] + ".git") != null) { + referenceExpanded = subEntry[0]; + break; + } + } + } + if (referenceExpanded == null) { + referenceExpanded = subEntries.iterator().next()[0]; + } + LOGGER.log( + Level.FINE, + "findParameterizedReferenceRepository(): got referenceExpanded='" + referenceExpanded + + "' from subEntries"); + if (reference.endsWith("/${GIT_SUBMODULES_FALLBACK}") + && getObjectsFile(referenceExpanded) == null + && getObjectsFile(referenceExpanded + ".git") == null) { + // chop it off, use main directory + referenceExpanded = referenceBaseDir; + } + } else { + LOGGER.log(Level.FINE, "findParameterizedReferenceRepository(): got no subEntries"); + // If there is no hit, the non-fallback mode suggests a new + // directory name to host the submodule (same rules as for + // the refrepo forks' co-hosting friendly basename search), + // and the fallback mode would return the main directory. + int sep = url.lastIndexOf("/"); + String needleBasename; + if (sep < 0) { + needleBasename = url; + } else { + needleBasename = url.substring(sep + 1); + } + needleBasename = needleBasename.replaceAll(".git$", ""); + + if (reference.endsWith("/${GIT_SUBMODULES}")) { + referenceExpanded = reference.replaceAll("\\$\\{GIT_SUBMODULES\\}$", needleBasename); + } else { // if (reference.endsWith("/${GIT_SUBMODULES_FALLBACK}")) { + referenceExpanded = reference.replaceAll("\\$\\{GIT_SUBMODULES\\}$", needleBasename); + if (reference.endsWith("/${GIT_SUBMODULES_FALLBACK}") + && getObjectsFile(referenceExpanded) == null + && getObjectsFile(referenceExpanded + ".git") == null) { + // chop it off, use main directory + referenceExpanded = referenceBaseDir; + } + } + } + } + + if (referenceExpanded != null) { + reference = referenceExpanded; + referencePath = null; // GC + referencePath = new File(reference); + } + + // Note: currently unit-tests expect this markup on stderr: + System.err.println("reference after='" + reference + "'\n"); + + LOGGER.log( + Level.INFO, + "After resolving the parameterized Git reference repository, " + "decided to use '" + reference + + "' directory for URL '" + url + "'"); + } // if referencePath is the replaceable token and not existing directory + + if (!referencePath.exists() && !reference.endsWith(".git")) { + // Normalize the URLs with or without .git suffix to + // be served by same dir with the refrepo contents + reference += ".git"; + referencePath = null; // GC + referencePath = new File(reference); + } + + // Note that the referenced path may exist or not exist, in the + // latter case it is up to the caller to decide on course of action. + // Maybe create this dir to begin a reference repo (given the options)? + return referencePath; + } + /** {@inheritDoc} */ @Override @Deprecated diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/RemoteGitImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/RemoteGitImpl.java index 6b0b65fccf..95e97d67ec 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/RemoteGitImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/RemoteGitImpl.java @@ -342,6 +342,18 @@ public String getRemoteUrl(String name) throws GitException, InterruptedExceptio return proxy.getRemoteUrl(name); } + /** {@inheritDoc} */ + @Override + public Map getRemoteUrls() throws GitException, InterruptedException { + return proxy.getRemoteUrls(); + } + + /** {@inheritDoc} */ + @Override + public Map getRemotePushUrls() throws GitException, InterruptedException { + return proxy.getRemotePushUrls(); + } + /** {@inheritDoc} */ @Override public void setRemoteUrl(String name, String url) throws GitException, InterruptedException { @@ -706,6 +718,11 @@ public GitClient subGit(String subdir) { return proxy.subGit(subdir); } + /** {@inheritDoc} */ + public GitClient newGit(String somedir) { + return proxy.newGit(somedir); + } + /** * hasGitModules. * diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/GitClientCloneTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/GitClientCloneTest.java index c81b9aa7ad..b4c6f39f9e 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/GitClientCloneTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/GitClientCloneTest.java @@ -269,6 +269,213 @@ public void test_clone_reference() throws Exception { is(true)); } + @Test + public void test_clone_reference_parameterized_basename() throws Exception, IOException, InterruptedException { + testGitClient + .clone_() + .url(workspace.localMirror()) + .repositoryName("origin") + .reference(workspace.localMirror() + "/${GIT_URL_BASENAME}") + .execute(); + testGitClient.checkout().ref("origin/master").branch("master").execute(); + check_remote_url(workspace, testGitClient, "origin"); + // Verify JENKINS-46737 expected log message is written + String messages = StringUtils.join(handler.getMessages(), ";"); + assertThat( + "Reference repo name-parsing logged in: " + messages, + handler.containsMessageSubstring("Parameterized reference path ") + && handler.containsMessageSubstring(" replaced with: "), + is(true)); + // TODO: Actually construct the local filesystem path to match + // the last pathname component from the URL (plus/minus ".git" + // extension). Be sure to clean away this path at end of test, + // so that the test_clone_reference_parameterized_basename_fallback() + // below is not confused - it expects this location to not exist. + // Skip: Missing if clone failed - currently would, with bogus + // path above and not yet pre-created path structure. + /* + assertThat("Reference repo logged in: " + messages, + handler.containsMessageSubstring( + "Using reference repository: "), is(true)); + assertAlternateFilePointsToLocalMirror(); + assertBranchesExist(testGitClient.getBranches(), "master"); + assertNoObjectsInRepository(); + */ + } + + @Test + public void test_clone_reference_parameterized_basename_fallback() + throws Exception, IOException, InterruptedException { + // TODO: Currently we do not make paths which would invalidate + // this test, but note the test above might do just that later. + testGitClient + .clone_() + .url(workspace.localMirror()) + .repositoryName("origin") + .reference(workspace.localMirror() + "/${GIT_URL_BASENAME_FALLBACK}") + .execute(); + testGitClient.checkout().ref("origin/master").branch("master").execute(); + check_remote_url(workspace, testGitClient, "origin"); + // Verify JENKINS-46737 expected log message is written + String messages = StringUtils.join(handler.getMessages(), ";"); + assertThat( + "Reference repo name-parsing logged in: " + messages, + handler.containsMessageSubstring("Parameterized reference path ") + && handler.containsMessageSubstring(" replaced with: '" + workspace.localMirror() + "'"), + is(true)); + // With fallback mode, and nonexistent parameterized reference + // repository, and a usable repository in the common path (what + // remains if the parameterizing suffix is just discarded), this + // common path should be used. So it should overall behave same + // as the non-parameterized test_clone_reference_basename() above. + assertThat( + "Reference repo logged in: " + messages, + handler.containsMessageSubstring("Using reference repository: "), + is(true)); + assertAlternateFilePointsToLocalMirror(); + assertBranchesExist(testGitClient.getBranches(), "master"); + assertNoObjectsInRepository(); + } + + @Test + public void test_clone_reference_parameterized_sha256() throws Exception, IOException, InterruptedException { + String wsMirror = workspace.localMirror(); + /* Same rules of URL normalization as in LegacyCompatibleGitAPIImpl.java + * should be okay for network URLs but are too complex for local pathnames */ + // String wsMirrorNormalized = wsMirror.replaceAll("/*$", "").replaceAll(".git$", "").toLowerCase(); + String wsMirrorNormalized = LegacyCompatibleGitAPIImpl.normalizeGitUrl(wsMirror, true); + String wsMirrorHash = org.apache.commons.codec.digest.DigestUtils.sha256Hex(wsMirrorNormalized); + + /* Make a new repo replica to use as refrepo, in specified location */ + // Start of the path to pass into `git clone` call; note that per + // WorkspaceWithRepo.java the test workspaces are under target/ + // where the executed test binaries live + File fRefrepoBase = new File("target/refrepo256.git").getAbsoluteFile(); + String wsRefrepoBase = fRefrepoBase.getPath(); // String with full pathname + String wsRefrepo = null; + try { + if (fRefrepoBase.exists() || fRefrepoBase.mkdirs()) { + /* Note: per parser of magic suffix, use slash - not OS separator char + * And be sure to use relative paths here (see + * WorkspaceWithRepo.java::localMirror()): + */ + wsRefrepo = workspace.localMirror("refrepo256.git/" + wsMirrorHash); + } + } catch (RuntimeException e) { + wsRefrepo = null; + Util.deleteRecursive(fRefrepoBase); + // At worst, the test would log that it mangled and parsed + // the provided string, as we check in log below + } + + System.err.println("wsRefrepoBase='" + wsRefrepoBase + "'\n" + "wsRefrepo='" + wsRefrepo); + + testGitClient + .clone_() + .url(wsMirror) + .repositoryName("origin") + .reference(wsRefrepoBase + "/${GIT_URL_SHA256}") + .execute(); + + testGitClient.checkout().ref("origin/master").branch("master").execute(); + check_remote_url(workspace, testGitClient, "origin"); + + // Verify JENKINS-46737 expected log message is written + String messages = StringUtils.join(handler.getMessages(), ";"); + + System.err.println("clone output:\n======\n" + messages + "\n======\n"); + + assertThat( + "Reference repo name-parsing logged in: " + messages + + (wsRefrepo == null ? "" : ("\n...and replaced with: '" + wsRefrepo + "'")), + handler.containsMessageSubstring("Parameterized reference path ") + && handler.containsMessageSubstring(" replaced with: ") + && (wsRefrepo == null || handler.containsMessageSubstring(wsRefrepo)), + is(true)); + + if (wsRefrepo != null) { + assertThat( + "Reference repo logged in: " + messages, + handler.containsMessageSubstring("Using reference repository: "), + is(true)); + assertAlternateFilePointsToLocalWorkspaceMirror(testGitDir.getPath(), wsRefrepo); + assertBranchesExist(testGitClient.getBranches(), "master"); + assertNoObjectsInRepository(); + } // else Skip: Missing if clone failed - currently would, + // with bogus path above and not pre-created path structure + } + + @Test + public void test_clone_reference_parameterized_sha256_fallback() + throws Exception, IOException, InterruptedException { + String wsMirror = workspace.localMirror(); + /* Same rules of URL normalization as in LegacyCompatibleGitAPIImpl.java + * should be okay for network URLs but are too complex for local pathnames */ + // String wsMirrorNormalized = wsMirror.replaceAll("/*$", "").replaceAll(".git$", "").toLowerCase(); + String wsMirrorNormalized = LegacyCompatibleGitAPIImpl.normalizeGitUrl(wsMirror, true); + String wsMirrorHash = org.apache.commons.codec.digest.DigestUtils.sha256Hex(wsMirrorNormalized); + + /* Make a new repo replica to use as refrepo, in specified location */ + // Start of the path to pass into `git clone` call; note that per + // WorkspaceWithRepo.java the test workspaces are under target/ + // where the executed test binaries live + File fRefrepoBase = new File("target/refrepo256.git").getAbsoluteFile(); + String wsRefrepoBase = fRefrepoBase.getPath(); // String with full pathname + String wsRefrepo = null; + try { + if (fRefrepoBase.exists() || fRefrepoBase.mkdirs()) { + /* Note: per parser of magic suffix, use slash - not OS separator char + * And be sure to use relative paths here (see + * WorkspaceWithRepo.java::localMirror()): + */ + wsRefrepo = workspace.localMirror("refrepo256.git/" + wsMirrorHash); + } + } catch (RuntimeException e) { + wsRefrepo = null; + Util.deleteRecursive(fRefrepoBase); + // At worst, the test would log that it mangled and parsed + // the provided string, as we check in log below + } + + System.err.println("wsRefrepoBase='" + wsRefrepoBase + "'\n" + "wsRefrepo='" + wsRefrepo); + + testGitClient + .clone_() + .url(wsMirror) + .repositoryName("origin") + .reference(wsRefrepoBase + "/${GIT_URL_SHA256_FALLBACK}") + .execute(); + + testGitClient.checkout().ref("origin/master").branch("master").execute(); + check_remote_url(workspace, testGitClient, "origin"); + + // Verify JENKINS-46737 expected log message is written + String messages = StringUtils.join(handler.getMessages(), ";"); + + System.err.println("clone output:\n======\n" + messages + "\n======\n"); + + // Note: we do not expect the closing single quote after wsRefrepoBase + // because other tests might pollute our test area, and SHA dir is there + assertThat( + "Reference repo name-parsing logged in: " + messages + "\n...and replaced with: '" + wsRefrepoBase, + handler.containsMessageSubstring("Parameterized reference path ") + && handler.containsMessageSubstring(" replaced with: '" + wsRefrepoBase), + is(true)); + + // Barring filesystem errors, if we have the "custom" refrepo + // we expect it to be used (fallback mode is not triggered) + if (wsRefrepo != null) { + assertThat( + "Reference repo logged in: " + messages, + handler.containsMessageSubstring("Using reference repository: "), + is(true)); + assertAlternateFilePointsToLocalWorkspaceMirror(testGitDir.getPath(), wsRefrepo); + assertBranchesExist(testGitClient.getBranches(), "master"); + assertNoObjectsInRepository(); + } // else Skip: Missing if clone failed - currently would, + // with bogus path above and not pre-created path structure + } + private static final String SRC_DIR = (new File(".")).getAbsolutePath(); @Test @@ -453,12 +660,59 @@ private void assertNoObjectsInRepository() { } } + // Most tests use this method, expecting a non-bare repo private void assertAlternateFilePointsToLocalMirror() throws IOException, InterruptedException { + assertAlternateFilePointsToLocalWorkspaceMirror(testGitDir); + } + + private void assertAlternateFilePointsToLocalWorkspaceMirror() throws IOException, InterruptedException { + assertAlternateFilePointsToLocalWorkspaceMirror(testGitDir); + } + + private void assertAlternateFilePointsToLocalWorkspaceMirror(File _testGitDir) + throws IOException, InterruptedException { + assertAlternateFilePointsToLocalWorkspaceMirror(_testGitDir.getPath()); + } + + private void assertAlternateFilePointsToLocalWorkspaceMirror(String _testGitDir) + throws IOException, InterruptedException { + assertAlternateFilePointsToLocalWorkspaceMirror(_testGitDir, workspace.localMirror()); + } + + private void assertAlternateFilePointsToLocalWorkspaceMirror(String _testGitDir, String _testAltDir) + throws IOException, InterruptedException { final String alternates = ".git" + File.separator + "objects" + File.separator + "info" + File.separator + "alternates"; + assertAlternateFilePointsToLocalMirror(_testGitDir, _testAltDir, alternates); + } - assertThat(new File(testGitDir, alternates), is(anExistingFile())); - final String expectedContent = workspace.localMirror().replace("\\", "/") + "/objects"; + // Similar for bare repos, without ".git/" dir + private void assertAlternateFilePointsToLocalBareMirror() throws IOException, InterruptedException { + assertAlternateFilePointsToLocalBareMirror(testGitDir); + } + + private void assertAlternateFilePointsToLocalBareMirror(File _testGitDir) throws IOException, InterruptedException { + assertAlternateFilePointsToLocalBareMirror(_testGitDir.getPath()); + } + + private void assertAlternateFilePointsToLocalBareMirror(String _testGitDir) + throws IOException, InterruptedException { + assertAlternateFilePointsToLocalBareMirror(_testGitDir, workspace.localMirror()); + } + + private void assertAlternateFilePointsToLocalBareMirror(String _testGitDir, String _testAltDir) + throws IOException, InterruptedException { + final String alternates = "objects" + File.separator + "info" + File.separator + "alternates"; + assertAlternateFilePointsToLocalMirror(_testGitDir, _testAltDir, alternates); + } + + private void assertAlternateFilePointsToLocalMirror(String _testGitDir, String _testAltDir, String alternates) + throws IOException, InterruptedException { + assertThat( + "Did not find '" + alternates + "' under '" + _testGitDir + "'", + new File(_testGitDir, alternates), + is(anExistingFile())); + final String expectedContent = _testAltDir.replace("\\", "/") + "/objects"; final String actualContent = Files.readString(testGitDir.toPath().resolve(alternates), StandardCharsets.UTF_8); assertThat("Alternates file content", actualContent, is(expectedContent)); final File alternatesDir = new File(actualContent); diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/WorkspaceWithRepo.java b/src/test/java/org/jenkinsci/plugins/gitclient/WorkspaceWithRepo.java index cca6cc1637..759507158b 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/WorkspaceWithRepo.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/WorkspaceWithRepo.java @@ -16,6 +16,7 @@ import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import org.apache.commons.lang.StringUtils; import org.eclipse.jgit.lib.ObjectId; @@ -85,12 +86,18 @@ private boolean isShallow() { * @throws InterruptedException when exception is interrupted */ String localMirror() throws IOException, InterruptedException { + return localMirror("clone.git"); + } + + String localMirror(String cloneDirName) throws IOException, InterruptedException { File base = new File(".").getAbsoluteFile(); + System.err.println("=== Beginning to search for cloneDirName='" + cloneDirName + "' from " + base.getPath()); for (File f = base; f != null; f = f.getParentFile()) { + System.err.println("Looking for 'target' in " + f.getPath()); File targetDir = new File(f, "target"); if (targetDir.exists()) { - String cloneDirName = "clone.git"; File clone = new File(targetDir, cloneDirName); + System.err.println("Looking for cloneDirName " + cloneDirName + " in " + targetDir.getPath()); if (!clone.exists()) { /* Clone to a temporary directory then move the * temporary directory to the final destination @@ -102,6 +109,7 @@ String localMirror() throws IOException, InterruptedException { */ Path tempClonePath = Files.createTempDirectory(targetDir.toPath(), "clone-"); String destination = tempClonePath.toFile().getAbsolutePath(); + System.err.println("tempClonePath=" + tempClonePath + " => (FQPN)" + destination); if (isShallow()) { cliGitCommand.run("clone", "--mirror", repoURL, destination); } else { @@ -109,6 +117,7 @@ String localMirror() throws IOException, InterruptedException { "clone", "--reference", f.getCanonicalPath(), "--mirror", repoURL, destination); } if (!clone.exists()) { // Still a race condition, but a narrow race handled by Files.move() + System.err.println("moving tempClonePath to cloneDirName=" + cloneDirName); renameAndDeleteDir(tempClonePath, cloneDirName); } else { /* @@ -133,10 +142,15 @@ String localMirror() throws IOException, InterruptedException { * deleteRecursive() will discard a clone that * 'lost the race'. */ + System.err.println( + "removing extra tempClonePath, we already (race?) have cloneDirName=" + cloneDirName); Util.deleteRecursive(tempClonePath.toFile()); } + } else { + System.err.println("FOUND cloneDirName " + cloneDirName + " in " + targetDir.getPath()); } - return clone.getPath(); + // Strip away the "/./" in "...git-client-plugin/./target/..." + return Paths.get(clone.getPath()).normalize().toString(); } } throw new IllegalStateException();