diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 827c6870..5c8b712d 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -20,6 +20,8 @@ jobs: with: java-version: ${{ matrix.Java }} distribution: temurin + - run: git config --global user.email "you@example.com" + - run: git config --global user.name "Your Name" - name: Build with Gradle uses: gradle/gradle-build-action@v2 with: diff --git a/README.md b/README.md index 2eab0d07..ade37487 100644 --- a/README.md +++ b/README.md @@ -249,3 +249,10 @@ GitHub. PRERELEASE=no ./gradlew clean build githubRelease ``` * Edit the release description on `GitHub` if necessary. + +## Acknowledgements + +This plugin contains Git sub-process handling code originally written and released under Apache 2 licenses by +* the GoCD core team +* [Srinivas Upadhya](https://github.com/srinivasupadhya) via https://github.com/srinivasupadhya/git-cmd/ +* [Ashwanth Kumar](https://github.com/ashwanthkumar) and [Chad Wilson](https://github.com/chadlwilson) via https://github.com/ashwanthkumar/git-cmd diff --git a/build.gradle b/build.gradle index 0256f5f5..ed7590fa 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ java { gocdPlugin { id = 'git-path' - pluginVersion = '2.2.2' + pluginVersion = '2.3.0' goCdVersion = '19.9.0' name = 'Git Path Material Plugin' description = 'Plugin that polls a Git repository and triggers pipelines based on sub-directory path matches' @@ -49,13 +49,9 @@ ext { dependencies { compileOnly "cd.go.plugin:go-plugin-api:${gocdPluginVersion}" - implementation 'in.ashwanthkumar:git-cmd:2.0' - constraints { - // Control transitive dependency versions from git-cmd - implementation 'org.eclipse.jgit:org.eclipse.jgit:6.6.0.202305301015-r' - implementation 'joda-time:joda-time:2.12.5' - implementation 'commons-io:commons-io:2.13.0' - } + implementation 'joda-time:joda-time:2.12.5' + implementation 'org.apache.commons:commons-exec:1.3' + implementation 'commons-io:commons-io:2.13.0' implementation platform('com.fasterxml.jackson:jackson-bom:2.15.2') implementation 'com.fasterxml.jackson.core:jackson-databind' @@ -69,6 +65,7 @@ dependencies { testCompileOnly "cd.go.plugin:go-plugin-api:${gocdPluginVersion}" testImplementation 'org.mockito:mockito-junit-jupiter:5.4.0' testImplementation 'org.assertj:assertj-core:3.24.2' + testImplementation 'uk.org.webcompere:system-stubs-jupiter:2.0.2' } test { diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/GitPathMaterialPlugin.java b/src/main/java/com/thoughtworks/go/scm/plugin/GitPathMaterialPlugin.java index 136e9d3e..82c7620a 100644 --- a/src/main/java/com/thoughtworks/go/scm/plugin/GitPathMaterialPlugin.java +++ b/src/main/java/com/thoughtworks/go/scm/plugin/GitPathMaterialPlugin.java @@ -4,8 +4,6 @@ import com.thoughtworks.go.plugin.api.GoPlugin; import com.thoughtworks.go.plugin.api.GoPluginIdentifier; import com.thoughtworks.go.plugin.api.annotation.Extension; -import com.thoughtworks.go.plugin.api.annotation.Load; -import com.thoughtworks.go.plugin.api.info.PluginContext; import com.thoughtworks.go.plugin.api.logging.Logger; import com.thoughtworks.go.plugin.api.request.GoPluginApiRequest; import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; @@ -18,7 +16,7 @@ public class GitPathMaterialPlugin implements GoPlugin { private static final String EXTENSION_NAME = "scm"; private static final List goSupportedVersions = Arrays.asList("1.0"); - private static Logger LOGGER = Logger.getLoggerFor(GitPathMaterialPlugin.class); + private static final Logger LOGGER = Logger.getLoggerFor(GitPathMaterialPlugin.class); @Override public void initializeGoApplicationAccessor(GoApplicationAccessor goApplicationAccessor) { @@ -42,10 +40,4 @@ public GoPluginApiResponse handle(GoPluginApiRequest apiRequest) { public GoPluginIdentifier pluginIdentifier() { return new GoPluginIdentifier(EXTENSION_NAME, goSupportedVersions); } - - @Load - public void onLoad(PluginContext context) { - LOGGER.info("Loading GitPathMaterialPlugin..."); - LOGGER.info("Type is {}", HelperFactory.determineType()); - } } diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/HelperFactory.java b/src/main/java/com/thoughtworks/go/scm/plugin/HelperFactory.java deleted file mode 100644 index ed920cfe..00000000 --- a/src/main/java/com/thoughtworks/go/scm/plugin/HelperFactory.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.thoughtworks.go.scm.plugin; - - -import com.thoughtworks.go.plugin.api.logging.Logger; -import com.tw.go.plugin.GitHelper; -import com.tw.go.plugin.cmd.InMemoryConsumer; -import com.tw.go.plugin.cmd.ProcessOutputStreamConsumer; -import com.tw.go.plugin.git.GitCmdHelper; -import com.tw.go.plugin.jgit.JGitHelper; -import com.tw.go.plugin.model.GitConfig; - -import java.io.File; -import java.util.concurrent.atomic.AtomicReference; - -public class HelperFactory { - private enum HelperType {CMD, JGIT} - - private static final Logger LOGGER = Logger.getLoggerFor(HelperFactory.class); - private static final AtomicReference HELPER_TYPE = new AtomicReference<>(null); - - public static GitHelper git(GitConfig gitConfig, File workingDirectory, ProcessOutputStreamConsumer stdOut, ProcessOutputStreamConsumer stdErr) { - if (determineType() == HelperType.CMD) { - return new GitCmdHelper(gitConfig, workingDirectory, stdOut, stdErr); - } else { - return new JGitHelper(gitConfig, workingDirectory, stdOut, stdErr); - } - } - - public static GitHelper git(GitConfig gitConfig, File workingDirectory) { - return git(gitConfig, - workingDirectory, - new ProcessOutputStreamConsumer(new InMemoryConsumer()), - new ProcessOutputStreamConsumer(new InMemoryConsumer()) - ); - } - - public static HelperType determineType() { - HelperType helperType = HELPER_TYPE.get(); - if (helperType == null) { - try { - LOGGER.info("Command line git found [{}]", new GitCmdHelper(null, null).version()); - helperType = HelperType.CMD; - } catch (Exception e) { - LOGGER.info("No command line git found; will continue with JGit [{}]", e.toString()); - helperType = HelperType.JGIT; - } - HELPER_TYPE.compareAndSet(null, helperType); - } - return helperType; - } - -} diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/git/GitConfig.java b/src/main/java/com/thoughtworks/go/scm/plugin/git/GitConfig.java new file mode 100644 index 00000000..8177543c --- /dev/null +++ b/src/main/java/com/thoughtworks/go/scm/plugin/git/GitConfig.java @@ -0,0 +1,150 @@ +package com.thoughtworks.go.scm.plugin.git; + +import com.thoughtworks.go.scm.plugin.util.StringUtil; + +import java.util.Objects; +import java.util.Optional; + +public class GitConfig { + private String url; + private String username; + private String password; + private String branch; + private boolean subModule = false; + private boolean recursiveSubModuleUpdate = true; + private boolean noCheckout = false; + private Optional shallowClone = Optional.empty(); + + public GitConfig(String url) { + this.url = url; + } + + public GitConfig(String url, String username, String password, String branch) { + this(url, username, password, branch, true, false); + } + + public GitConfig(String url, String username, String password, String branch, boolean recursiveSubModuleUpdate, boolean shallowClone) { + this.url = url; + this.username = username; + this.password = password; + this.branch = branch; + this.recursiveSubModuleUpdate = recursiveSubModuleUpdate; + this.shallowClone = shallowClone ? Optional.of(new ShallowClone()) : Optional.empty(); + } + + public boolean isRemoteUrl() { + return url.startsWith("http://") || url.startsWith("https://"); + } + + public boolean hasCredentials() { + return !StringUtil.isBlank(url) && !StringUtil.isBlank(password); + } + + public String getEffectiveUrl() { + if (isRemoteUrl() && hasCredentials()) { + return getUrlWithCredentials(); + } + return getUrl(); + } + + public String getUrlWithCredentials() { + String[] parts = url.split("://"); + return String.format("%s://%s:%s@%s", parts[0], username, password, parts[1]); + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getRemoteBranch() { + return String.format("origin/%s", getEffectiveBranch()); + } + + public String getEffectiveBranch() { + return StringUtil.isBlank(branch) ? "master" : branch; + } + + public String getBranch() { + return branch; + } + + public void setBranch(String branch) { + this.branch = branch; + } + + public boolean isSubModule() { + return subModule; + } + + public void setSubModule(boolean subModule) { + this.subModule = subModule; + } + + public boolean isRecursiveSubModuleUpdate() { + return recursiveSubModuleUpdate; + } + + public void setRecursiveSubModuleUpdate(boolean recursiveSubModuleUpdate) { + this.recursiveSubModuleUpdate = recursiveSubModuleUpdate; + } + + public boolean isShallowClone() { + return shallowClone.isPresent(); + } + + public Optional getShallowClone() { + return shallowClone; + } + + public void setShallowClone(ShallowClone shallowClone) { + this.shallowClone = Optional.of(shallowClone); + } + + public boolean isNoCheckout() { + return noCheckout; + } + + public void setNoCheckout(boolean noCheckout) { + this.noCheckout = noCheckout; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GitConfig gitConfig = (GitConfig) o; + return subModule == gitConfig.subModule && + recursiveSubModuleUpdate == gitConfig.recursiveSubModuleUpdate && + noCheckout == gitConfig.noCheckout && + Objects.equals(url, gitConfig.url) && + Objects.equals(username, gitConfig.username) && + Objects.equals(password, gitConfig.password) && + Objects.equals(branch, gitConfig.branch) && + shallowClone.equals(gitConfig.shallowClone); + } + + @Override + public int hashCode() { + return Objects.hash(url, username, password, branch, subModule, recursiveSubModuleUpdate, noCheckout, shallowClone); + } +} diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/git/GitHelper.java b/src/main/java/com/thoughtworks/go/scm/plugin/git/GitHelper.java new file mode 100644 index 00000000..ea2821cb --- /dev/null +++ b/src/main/java/com/thoughtworks/go/scm/plugin/git/GitHelper.java @@ -0,0 +1,522 @@ +package com.thoughtworks.go.scm.plugin.git; + +import com.thoughtworks.go.scm.plugin.git.cmd.Console; +import com.thoughtworks.go.scm.plugin.git.cmd.ConsoleResult; +import com.thoughtworks.go.scm.plugin.git.cmd.InMemoryConsumer; +import com.thoughtworks.go.scm.plugin.git.cmd.ProcessOutputStreamConsumer; +import com.thoughtworks.go.scm.plugin.util.StringUtil; +import org.apache.commons.exec.CommandLine; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import static java.util.stream.Stream.concat; +import static java.util.stream.Stream.of; + +public class GitHelper { + public static final String GIT_SUBMODULE_ALLOW_FILE_PROTOCOL = "toggle.git.submodule.allow.file.protocol"; + private static final Pattern GIT_SUBMODULE_STATUS_PATTERN = Pattern.compile("^.[0-9a-fA-F]{40} (.+?)( \\(.+\\))?$"); + private static final Pattern GIT_SUBMODULE_URL_PATTERN = Pattern.compile("^submodule\\.(.+)\\.url (.+)$"); + private static final Pattern GIT_DIFF_TREE_PATTERN = Pattern.compile("^(.{1,3})\\s+(.+)$"); + + private final GitConfig gitConfig; + private final File workingDir; + private final ProcessOutputStreamConsumer stdOut; + private final ProcessOutputStreamConsumer stdErr; + + + public GitHelper(GitConfig gitConfig, File workingDir) { + this(gitConfig, workingDir, new ProcessOutputStreamConsumer(new InMemoryConsumer()), new ProcessOutputStreamConsumer(new InMemoryConsumer())); + } + + public GitHelper(GitConfig gitConfig, File workingDir, ProcessOutputStreamConsumer stdOut, ProcessOutputStreamConsumer stdErr) { + this.gitConfig = gitConfig; + this.workingDir = workingDir; + this.stdOut = stdOut; + this.stdErr = stdErr; + } + + public String version() { + CommandLine gitCmd = Console.createCommand("--version"); + return runAndGetOutput(gitCmd, new File("/")).stdOut().get(0); + } + + public void checkConnection() { + CommandLine gitCmd = Console.createCommand("ls-remote", gitConfig.getEffectiveUrl()); + runAndGetOutput(gitCmd); + } + + public void cloneRepository() { + List args = new ArrayList<>(Arrays.asList("clone", String.format("--branch=%s", gitConfig.getEffectiveBranch()))); + if (gitConfig.isNoCheckout()) { + args.add("--no-checkout"); + } + + gitConfig.getShallowClone() + .ifPresent(settings -> args.add("--depth=" + settings.getDefaultCommitsDepth())); + + args.add(gitConfig.getEffectiveUrl()); + args.add(workingDir.getAbsolutePath()); + CommandLine gitClone = Console.createCommand(args.toArray(new String[0])); + runAndGetOutput(gitClone, null, stdOut, stdErr); + } + + public void checkoutRemoteBranchToLocal() { + CommandLine gitCmd = Console.createCommand("checkout", "-f", gitConfig.getEffectiveBranch()); + runOrBomb(gitCmd); + } + + public String workingRepositoryUrl() { + CommandLine gitConfig = Console.createCommand("config", "remote.origin.url"); + return runAndGetOutput(gitConfig).stdOut().get(0); + } + + public String getCurrentBranch() { + CommandLine gitRevParse = Console.createCommand("rev-parse", "--abbrev-ref", "HEAD"); + return runAndGetOutput(gitRevParse).stdOut().get(0); + } + + public int getCommitCount() { + CommandLine gitCmd = Console.createCommand("rev-list", "HEAD", "--count"); + return Integer.parseInt(runAndGetOutput(gitCmd).stdOut().get(0)); + } + + public String currentRevision() { + CommandLine gitLog = Console.createCommand("log", "-1", "--pretty=format:%H", "--no-decorate", "--no-color"); + return runAndGetOutput(gitLog).stdOut().stream().findFirst().orElse(null); + } + + public List getAllRevisions() { + return gitLog(logArgs()); + } + + public Revision getLatestRevision() { + return getLatestRevision(null); + } + + public Revision getLatestRevision(List subPaths) { + return gitLog(logArgs(subPaths, "-1")) + .stream() + .findFirst() + .orElse(null); + } + + public List getRevisionsSince(String revision) { + return getRevisionsSince(revision, null); + } + + public List getRevisionsSince(String revision, List subPaths) { + return gitLog(logArgs(subPaths, String.format("%s..%s", revision, gitConfig.getRemoteBranch()))); + } + + private String[] logArgs(String... revisionLimits) { + return logArgs(null, revisionLimits); + } + + private String[] logArgs(List subPaths, String... revisionLimits) { + String[] logs = of( + of("log", "--date=iso", "--pretty=medium", "--no-decorate", "--no-color"), + of(revisionLimits), + Stream.ofNullable(subPaths).flatMap(paths -> of("--")), + Stream.ofNullable(subPaths).flatMap(paths -> subPaths.stream().map(String::trim)) + ) + .flatMap(s -> s) + .filter(Objects::nonNull) + .toArray(String[]::new); + return logs; + } + + public Revision getDetailsForRevision(String sha) { + return gitLog(logArgs("-1", sha)) + .stream() + .findFirst() + .orElse(null); + } + + public Map getBranchToRevisionMap(String pattern) { + CommandLine gitCmd = Console.createCommand("show-ref"); + List outputLines = runAndGetOutput(gitCmd).stdOut(); + Map branchToRevisionMap = new HashMap<>(); + for (String line : outputLines) { + if (line.contains(pattern)) { + String[] parts = line.split(" "); + String branch = parts[1].replace(pattern, ""); + String revision = parts[0]; + if (!branch.equals("HEAD")) { + branchToRevisionMap.put(branch, revision); + } + } + } + return branchToRevisionMap; + } + + private List gitLog(String... args) { + CommandLine gitLog = Console.createCommand(args); + List gitLogOutput = runAndGetOutput(gitLog).stdOut(); + + List revisions = new GitModificationParser().parse(gitLogOutput); + for (Revision revision : revisions) { + addModifiedFiles(revision); + } + return revisions; + } + + private void addModifiedFiles(Revision revision) { + List diffTreeOutput = diffTree(revision.getRevision()).stdOut(); + + for (String resultLine : diffTreeOutput) { + // First line is the node + if (resultLine.equals(revision.getRevision())) { + continue; + } + + Matcher m = matchResultLine(resultLine); + if (!m.find()) { + throw new RuntimeException(String.format("Unable to parse git-diff-tree output line: %s%nFrom output:%n %s", resultLine, String.join(System.lineSeparator(), diffTreeOutput))); + } + revision.createModifiedFile(m.group(2), parseGitAction(m.group(1).charAt(0))); + } + } + + private ConsoleResult diffTree(String node) { + CommandLine gitCmd = Console.createCommand("diff-tree", "--name-status", "--root", "-r", "-c", node); + return runAndGetOutput(gitCmd); + } + + private Matcher matchResultLine(String resultLine) { + return GIT_DIFF_TREE_PATTERN.matcher(resultLine); + } + + private String parseGitAction(char action) { + switch (action) { + case 'A': + return "added"; + case 'M': + return "modified"; + case 'D': + return "deleted"; + default: + return "unknown"; + } + } + + // http://www.kernel.org/pub/software/scm/git/docs/git-log.html + private String modificationTemplate(String separator) { + return "%cn <%ce>%n%H%n%ai%n%n%s%n%b%n" + separator; + } + + public void pull() { + CommandLine gitCommit = Console.createCommand("pull"); + runOrBomb(gitCommit); + } + + public void fetch(String refSpec) { + stdOut.consumeLine("[GIT] Fetching changes"); + List args = new ArrayList<>(Arrays.asList("fetch", "origin", "--prune", "--recurse-submodules=no")); + if (!StringUtil.isBlank(refSpec)) { + args.add(refSpec); + } + runOrBomb(Console.createCommand(args.toArray(new String[0]))); + } + + private void fetchToDepth(int depth) { + stdOut.consumeLine(String.format("[GIT] Fetching to commit depth %s", depth == Integer.MAX_VALUE ? "[INFINITE]" : depth)); + runOrBomb(Console.createCommand("fetch", "origin", "--depth=" + depth, "--recurse-submodules=no")); + } + + public void resetHard(String revision) { + gitConfig.getShallowClone().ifPresent(settings -> unshallowIfNecessary(settings.getAdditionalFetchDepth(), revision)); + + stdOut.consumeLine("[GIT] Updating working copy to revision " + revision); + CommandLine gitResetHard = Console.createCommand("reset", "--hard", revision); + runOrBomb(gitResetHard); + } + + private void unshallowIfNecessary(int additionalFetchDepth, String revision) { + if (branchContains(revision)) { + return; + } + + stdOut.consumeLine("[GIT] Working copy is shallow clone missing revision " + revision); + fetchToDepth(additionalFetchDepth); + + if (branchContains(revision)) { + return; + } + stdOut.consumeLine("[GIT] Working copy is shallow clone still missing revision " + revision + ", fetching full repo..."); + fetchToDepth(Integer.MAX_VALUE); + } + + private boolean branchContains(String revision) { + try { + ConsoleResult result = runAndGetOutput(Console.createCommand("branch", "-r", "--contains", revision)); + return result.stdOut().stream().anyMatch(line -> line.contains(gitConfig.getRemoteBranch())); + } catch (Exception ignore) { + return false; + } + } + + protected boolean shouldReset() { + return !gitConfig.isNoCheckout(); + } + + public void cleanAllUnversionedFiles() { + stdOut.consumeLine("[GIT] Cleaning all unversioned files in working copy"); + if (isSubmoduleEnabled()) { + for (Map.Entry submoduleFolder : submoduleUrls().entrySet()) { + cleanUnversionedFiles(new File(workingDir, submoduleFolder.getKey())); + } + } + cleanUnversionedFiles(workingDir); + } + + private void cleanUnversionedFiles(File workingDir) { + CommandLine gitClean = Console.createCommand("clean", "-dff"); + runAndGetOutput(gitClean, workingDir, stdOut, stdErr); + } + + public void gc() { + stdOut.consumeLine("[GIT] Performing git gc"); + runOrBomb(Console.createCommand("gc", "--auto")); + } + + public Map submoduleUrls() { + CommandLine gitConfig = Console.createCommand("config", "--get-regexp", "^submodule\\..+\\.url"); + List submoduleList = new ArrayList<>(); + try { + submoduleList = runAndGetOutput(gitConfig).stdOut(); + } catch (Exception e) { + // ignore + } + Map submoduleUrls = new HashMap<>(); + for (String submoduleLine : submoduleList) { + Matcher m = GIT_SUBMODULE_URL_PATTERN.matcher(submoduleLine); + if (!m.find()) { + throw new RuntimeException(String.format("Unable to parse git-config output line: %s%nFrom output:%n%s", submoduleLine, String.join(System.lineSeparator(), submoduleList))); + } + submoduleUrls.put(m.group(1), m.group(2)); + } + return submoduleUrls; + } + + public List submoduleFolders() { + CommandLine gitCmd = Console.createCommand("submodule", "status"); + return submoduleFolders(runAndGetOutput(gitCmd).stdOut()); + } + + private List submoduleFolders(List submoduleLines) { + List submoduleFolders = new ArrayList<>(); + for (String submoduleLine : submoduleLines) { + Matcher m = GIT_SUBMODULE_STATUS_PATTERN.matcher(submoduleLine); + if (!m.find()) { + throw new RuntimeException(String.format("Unable to parse git-submodule output line: %s%nFrom output:%n%s", submoduleLine, String.join(System.lineSeparator(), submoduleLines))); + } + submoduleFolders.add(m.group(1)); + } + return submoduleFolders; + } + + public void printSubmoduleStatus() { + stdOut.consumeLine("[GIT] Git sub-module status"); + CommandLine gitSubModuleStatus = Console.createCommand("submodule", "status"); + runOrBomb(gitSubModuleStatus); + } + + public void checkoutAllModifiedFilesInSubmodules() { + stdOut.consumeLine("[GIT] Removing modified files in submodules"); + CommandLine gitSubmoduleCheckout = Console.createCommand("submodule", "foreach", "--recursive", "git", "checkout", "."); + runOrBomb(gitSubmoduleCheckout); + } + + public int getSubModuleCommitCount(String subModuleFolder) { + CommandLine gitCmd = Console.createCommand("rev-list", "HEAD", "--count"); + return Integer.parseInt(runAndGetOutput(gitCmd, new File(workingDir, subModuleFolder)).stdOut().get(0)); + } + + public void submoduleInit() { + CommandLine gitSubModuleInit = Console.createCommand("submodule", "init"); + runOrBomb(gitSubModuleInit); + } + + public void submoduleSync() { + CommandLine gitSubModuleSync = Console.createCommand("submodule", "sync"); + runOrBomb(gitSubModuleSync); + + CommandLine gitSubModuleForEachSync = Console.createCommand("submodule", "foreach", "--recursive", "git", "submodule", "sync"); + runOrBomb(gitSubModuleForEachSync); + } + + public void submoduleUpdate() { + CommandLine gitSubModuleUpdate = Console.createCommand(concat(gitSubmoduleConfigArgs().stream(), of("submodule", "update")).toArray(String[]::new)); + runOrBomb(gitSubModuleUpdate); + } + + public void init() { + CommandLine gitCmd = Console.createCommand("init"); + runOrBomb(gitCmd); + } + + public void add(File fileToAdd) { + CommandLine gitAdd = Console.createCommand("add", fileToAdd.getName()); + runOrBomb(gitAdd); + } + + public void commit(String message) { + CommandLine gitCommit = Console.createCommand("commit", "-m", message); + runOrBomb(gitCommit); + } + + public void submoduleAdd(String repoUrl, String submoduleNameToPutInGitSubmodules, String folder) { + + String[] addSubmoduleWithSameNameArgs = concat(gitSubmoduleConfigArgs().stream(), of("submodule", "add", repoUrl, folder)).toArray(String[]::new); + runOrBomb(Console.createCommand(addSubmoduleWithSameNameArgs)); + + String[] changeSubmoduleNameInGitModules = new String[]{"config", "--file", ".gitmodules", "--rename-section", "submodule." + folder, "submodule." + submoduleNameToPutInGitSubmodules}; + runOrBomb(Console.createCommand(changeSubmoduleNameInGitModules)); + + String[] addGitModules = new String[]{"add", ".gitmodules"}; + runOrBomb(Console.createCommand(addGitModules)); + } + + private List gitSubmoduleConfigArgs() { + if ("Y".equalsIgnoreCase(System.getProperty(GIT_SUBMODULE_ALLOW_FILE_PROTOCOL))) { + return List.of("-c", "protocol.file.allow=always"); + } else { + return Collections.emptyList(); + } + } + + public void removeSubmoduleSectionsFromGitConfig() { + stdOut.consumeLine("[GIT] Cleaning submodule configurations in .git/config"); + for (String submoduleFolder : submoduleUrls().keySet()) { + configRemoveSection("submodule." + submoduleFolder); + } + } + + public void submoduleRemove(String folderName) { + configRemoveSection("submodule." + folderName); + + CommandLine gitConfig = Console.createCommand("config", "-f", ".gitmodules", "--remove-section", "submodule." + folderName); + runOrBomb(gitConfig); + + CommandLine gitRm = Console.createCommand("rm", "--cached", folderName); + runOrBomb(gitRm); + + FileUtils.deleteQuietly(new File(workingDir, folderName)); + } + + private void configRemoveSection(String section) { + CommandLine gitCmd = Console.createCommand("config", "--remove-section", section); + runOrBomb(gitCmd); + } + + public void changeSubmoduleUrl(String submoduleName, String newUrl) { + CommandLine gitConfig = Console.createCommand("config", "--file", ".gitmodules", "submodule." + submoduleName + ".url", newUrl); + runOrBomb(gitConfig); + } + + public void push() { + CommandLine gitCommit = Console.createCommand("push"); + runOrBomb(gitCommit); + } + + private void runOrBomb(CommandLine gitCmd) { + runAndGetOutput(gitCmd, workingDir, stdOut, stdErr); + } + + private ConsoleResult runAndGetOutput(CommandLine gitCmd) { + return runAndGetOutput(gitCmd, workingDir); + } + + private ConsoleResult runAndGetOutput(CommandLine gitCmd, File workingDir) { + return runAndGetOutput(gitCmd, workingDir, new ProcessOutputStreamConsumer(new InMemoryConsumer()), new ProcessOutputStreamConsumer(new InMemoryConsumer())); + } + + private ConsoleResult runAndGetOutput(CommandLine gitCmd, File workingDir, ProcessOutputStreamConsumer stdOut, ProcessOutputStreamConsumer stdErr) { + return Console.runOrBomb(gitCmd, workingDir, stdOut, stdErr); + } + + public void cloneOrFetch() { + cloneOrFetch(null); + } + + public void cloneOrFetch(String refSpec) { + if (!isGitRepository() || !isSameRepository()) { + setupWorkingDir(); + cloneRepository(); + } + + fetchAndResetToHead(refSpec); + } + + private boolean isGitRepository() { + File dotGit = new File(workingDir, ".git"); + return workingDir.exists() && dotGit.exists() && dotGit.isDirectory(); + } + + public boolean isSameRepository() { + try { + return workingRepositoryUrl().equals(gitConfig.getEffectiveUrl()); + } catch (Exception e) { + return false; + } + } + + private void setupWorkingDir() { + FileUtils.deleteQuietly(workingDir); + try { + FileUtils.forceMkdir(workingDir); + } catch (IOException e) { + throw new RuntimeException("Could not create directory: " + workingDir.getAbsolutePath()); + } + } + + public Map getBranchToRevisionMap() { + return getBranchToRevisionMap("refs/remotes/origin/"); + } + + public void fetchAndResetToHead(String refSpec) { + fetchAndReset(refSpec, gitConfig.getRemoteBranch()); + } + + public void fetchAndReset(String refSpec, String revision) { + fetch(refSpec); + gc(); + + if (shouldReset()) { + stdOut.consumeLine(String.format("[GIT] Reset working directory %s", workingDir)); + cleanAllUnversionedFiles(); + if (isSubmoduleEnabled()) { + removeSubmoduleSectionsFromGitConfig(); + } + resetHard(revision); + if (isSubmoduleEnabled()) { + checkoutAllModifiedFilesInSubmodules(); + updateSubmoduleWithInit(); + } + cleanAllUnversionedFiles(); + } + } + + public boolean isSubmoduleEnabled() { + return new File(workingDir, ".gitmodules").exists(); + } + + public void updateSubmoduleWithInit() { + stdOut.consumeLine("[GIT] Updating git sub-modules"); + + submoduleInit(); + + submoduleSync(); + + submoduleUpdate(); + + stdOut.consumeLine("[GIT] Cleaning unversioned files and sub-modules"); + printSubmoduleStatus(); + } +} diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/git/GitModificationParser.java b/src/main/java/com/thoughtworks/go/scm/plugin/git/GitModificationParser.java new file mode 100644 index 00000000..35e43372 --- /dev/null +++ b/src/main/java/com/thoughtworks/go/scm/plugin/git/GitModificationParser.java @@ -0,0 +1,58 @@ +package com.thoughtworks.go.scm.plugin.git; + +import com.thoughtworks.go.scm.plugin.util.DateUtils; + +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class GitModificationParser { + private static final String SPACES = "\\s+"; + private static final String COMMENT_INDENT = "\\s{4}"; + private static final String COMMENT_TEXT = "(.*)"; + private static final String HASH = "(\\w+)"; + private static final String DATE = "(.+)"; + private static final String AUTHOR = "(.+)"; + private static final String MULTIPLE_HASHES = "(.+)"; + private static final Pattern COMMIT_PATTERN = Pattern.compile("^commit" + SPACES + HASH + "$"); + private static final Pattern MERGE_PATTERN = Pattern.compile("^Merge:" + SPACES + MULTIPLE_HASHES + "$"); + private static final Pattern AUTHOR_PATTERN = Pattern.compile("^Author:" + SPACES + AUTHOR + "$"); + private static final Pattern DATE_PATTERN = Pattern.compile("^Date:" + SPACES + DATE + "$"); + private static final Pattern COMMENT_PATTERN = Pattern.compile("^" + COMMENT_INDENT + COMMENT_TEXT + "$"); + + private final LinkedList revisions = new LinkedList<>(); + + public List parse(List output) { + for (String line : output) { + processLine(line); + } + return revisions; + } + + public void processLine(String line) { + Matcher matcher = COMMIT_PATTERN.matcher(line); + if (matcher.matches()) { + revisions.add(new Revision(matcher.group(1), null, "", "", null, null)); + } + Matcher mergeMatcher = MERGE_PATTERN.matcher(line); + if(mergeMatcher.matches()) { + revisions.getLast().setMergeCommit(true); + } + Matcher authorMatcher = AUTHOR_PATTERN.matcher(line); + if (authorMatcher.matches()) { + revisions.getLast().setUser(authorMatcher.group(1)); + } + Matcher dateMatcher = DATE_PATTERN.matcher(line); + if (dateMatcher.matches()) { + revisions.getLast().setTimestamp(DateUtils.parseISO8601(dateMatcher.group(1))); + } + Matcher commentMatcher = COMMENT_PATTERN.matcher(line); + if (commentMatcher.matches()) { + Revision last = revisions.getLast(); + String comment = last.getComment(); + if (!comment.isEmpty()) comment += "\n"; + last.setComment(comment + commentMatcher.group(1)); + } + } +} diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/git/HelperFactory.java b/src/main/java/com/thoughtworks/go/scm/plugin/git/HelperFactory.java new file mode 100644 index 00000000..1d8d0438 --- /dev/null +++ b/src/main/java/com/thoughtworks/go/scm/plugin/git/HelperFactory.java @@ -0,0 +1,17 @@ +package com.thoughtworks.go.scm.plugin.git; + + +import com.thoughtworks.go.scm.plugin.git.cmd.ProcessOutputStreamConsumer; + +import java.io.File; + +public class HelperFactory { + + public static GitHelper git(GitConfig gitConfig, File workingDirectory, ProcessOutputStreamConsumer stdOut, ProcessOutputStreamConsumer stdErr) { + return new GitHelper(gitConfig, workingDirectory, stdOut, stdErr); + } + + public static GitHelper git(GitConfig gitConfig, File workingDirectory) { + return new GitHelper(gitConfig, workingDirectory); + } +} diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/git/ModifiedFile.java b/src/main/java/com/thoughtworks/go/scm/plugin/git/ModifiedFile.java new file mode 100644 index 00000000..29e5b402 --- /dev/null +++ b/src/main/java/com/thoughtworks/go/scm/plugin/git/ModifiedFile.java @@ -0,0 +1,44 @@ +package com.thoughtworks.go.scm.plugin.git; + +import java.util.Objects; + +public class ModifiedFile { + private final String fileName; + private final String action; + + public ModifiedFile(String fileName, String action) { + this.fileName = fileName; + this.action = action; + } + + public String getFileName() { + return fileName; + } + + public String getAction() { + return action; + } + + @Override + public String toString() { + return String.format("ModifiedFile{fileName='%s', action='%s'}", fileName, action); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ModifiedFile that = (ModifiedFile) o; + + if (!Objects.equals(fileName, that.fileName)) return false; + return Objects.equals(action, that.action); + } + + @Override + public int hashCode() { + int result = fileName != null ? fileName.hashCode() : 0; + result = 31 * result + (action != null ? action.hashCode() : 0); + return result; + } +} diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/git/Revision.java b/src/main/java/com/thoughtworks/go/scm/plugin/git/Revision.java new file mode 100644 index 00000000..265138b1 --- /dev/null +++ b/src/main/java/com/thoughtworks/go/scm/plugin/git/Revision.java @@ -0,0 +1,95 @@ +package com.thoughtworks.go.scm.plugin.git; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +public class Revision { + private String revision; + private Date timestamp; + private String comment; + private String user; + private String emailId; + private List modifiedFiles; + private boolean isMergeCommit; + + public Revision(String revision) { + this.revision = revision; + this.isMergeCommit = false; + } + + public Revision(String revision, Date timestamp, String comment, String user, String emailId, List modifiedFiles) { + this.revision = revision; + this.timestamp = timestamp; + this.comment = comment; + this.user = user; + this.emailId = emailId; + this.modifiedFiles = modifiedFiles; + this.isMergeCommit = false; + } + + public String getRevision() { + return revision; + } + + public void setRevision(String revision) { + this.revision = revision; + } + + public Date getTimestamp() { + return timestamp; + } + + public void setTimestamp(Date timestamp) { + this.timestamp = timestamp; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public String getUser() { + return user; + } + + public void setUser(String user) { + this.user = user; + } + + public String getEmailId() { + return emailId; + } + + public void setEmailId(String emailId) { + this.emailId = emailId; + } + + public List getModifiedFiles() { + return modifiedFiles; + } + + public void setModifiedFiles(List modifiedFiles) { + this.modifiedFiles = modifiedFiles; + } + + public final ModifiedFile createModifiedFile(String filename, String action) { + ModifiedFile file = new ModifiedFile(filename, action); + if (modifiedFiles == null) { + modifiedFiles = new ArrayList<>(); + } + modifiedFiles.add(file); + return file; + } + + public boolean isMergeCommit() { + return isMergeCommit; + } + + public void setMergeCommit(boolean mergeCommit) { + isMergeCommit = mergeCommit; + } +} diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/git/ShallowClone.java b/src/main/java/com/thoughtworks/go/scm/plugin/git/ShallowClone.java new file mode 100644 index 00000000..7a30fb81 --- /dev/null +++ b/src/main/java/com/thoughtworks/go/scm/plugin/git/ShallowClone.java @@ -0,0 +1,42 @@ +package com.thoughtworks.go.scm.plugin.git; + +import java.util.Objects; + +public class ShallowClone { + private final int defaultCommitsDepth; + private final int additionalFetchDepth; + + public ShallowClone() { + this(2, 100); + } + + public ShallowClone(int defaultCommitsDepth, int additionalFetchDepth) { + this.defaultCommitsDepth = defaultCommitsDepth; + this.additionalFetchDepth = additionalFetchDepth; + if (additionalFetchDepth <= defaultCommitsDepth) { + throw new IllegalArgumentException(String.format("Additional fetch depth must be greater than default (%s)", defaultCommitsDepth)); + } + } + + public int getAdditionalFetchDepth() { + return additionalFetchDepth; + } + + public int getDefaultCommitsDepth() { + return defaultCommitsDepth; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ShallowClone that = (ShallowClone) o; + return defaultCommitsDepth == that.defaultCommitsDepth && + additionalFetchDepth == that.additionalFetchDepth; + } + + @Override + public int hashCode() { + return Objects.hash(defaultCommitsDepth, additionalFetchDepth); + } +} diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/git/cmd/Console.java b/src/main/java/com/thoughtworks/go/scm/plugin/git/cmd/Console.java new file mode 100644 index 00000000..48427815 --- /dev/null +++ b/src/main/java/com/thoughtworks/go/scm/plugin/git/cmd/Console.java @@ -0,0 +1,35 @@ +package com.thoughtworks.go.scm.plugin.git.cmd; + +import org.apache.commons.exec.CommandLine; +import org.apache.commons.exec.DefaultExecutor; +import org.apache.commons.exec.Executor; +import org.apache.commons.exec.PumpStreamHandler; + +import java.io.File; + +public class Console { + public static CommandLine createCommand(String... args) { + CommandLine gitCmd = new CommandLine("git"); + gitCmd.addArguments(args, false); + return gitCmd; + } + + public static ConsoleResult runOrBomb(CommandLine commandLine, File workingDir, ProcessOutputStreamConsumer stdOut, ProcessOutputStreamConsumer stdErr) { + Executor executor = new DefaultExecutor(); + executor.setStreamHandler(new PumpStreamHandler(stdOut, stdErr)); + if (workingDir != null) { + executor.setWorkingDirectory(workingDir); + } + + try { + int exitCode = executor.execute(commandLine); + return new ConsoleResult(exitCode, stdOut.output(), stdErr.output()); + } catch (Exception e) { + throw new RuntimeException(getMessage(String.format("Exception (%s)", e.getMessage()), commandLine, workingDir), e); + } + } + + private static String getMessage(String type, CommandLine commandLine, File workingDir) { + return String.format("%s Occurred: %s - %s", type, commandLine.toString(), workingDir); + } +} diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/git/cmd/ConsoleResult.java b/src/main/java/com/thoughtworks/go/scm/plugin/git/cmd/ConsoleResult.java new file mode 100644 index 00000000..b57f5e5e --- /dev/null +++ b/src/main/java/com/thoughtworks/go/scm/plugin/git/cmd/ConsoleResult.java @@ -0,0 +1,27 @@ +package com.thoughtworks.go.scm.plugin.git.cmd; + +import java.util.List; + +public class ConsoleResult { + private final int exitCode; + private final List stdOut; + private final List stdErr; + + public ConsoleResult(int exitCode, List stdOut, List stdErr) { + this.exitCode = exitCode; + this.stdOut = stdOut; + this.stdErr = stdErr; + } + + public int exitCode() { + return exitCode; + } + + public List stdOut() { + return stdOut; + } + + public List stdErr() { + return stdErr; + } +} diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/git/cmd/InMemoryConsumer.java b/src/main/java/com/thoughtworks/go/scm/plugin/git/cmd/InMemoryConsumer.java new file mode 100644 index 00000000..706310c2 --- /dev/null +++ b/src/main/java/com/thoughtworks/go/scm/plugin/git/cmd/InMemoryConsumer.java @@ -0,0 +1,27 @@ +package com.thoughtworks.go.scm.plugin.git.cmd; + +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +public class InMemoryConsumer implements StreamConsumer { + private final Queue lines = new ConcurrentLinkedQueue<>(); + + @Override + public void consumeLine(String line) { + try { + lines.add(line); + } catch (RuntimeException ignore) { + } + } + + @Override + public List asList() { + return new ArrayList<>(lines); + } + + public String toString() { + return String.join(System.lineSeparator(), lines); + } +} diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/git/cmd/ProcessOutputStreamConsumer.java b/src/main/java/com/thoughtworks/go/scm/plugin/git/cmd/ProcessOutputStreamConsumer.java new file mode 100644 index 00000000..1592d7f4 --- /dev/null +++ b/src/main/java/com/thoughtworks/go/scm/plugin/git/cmd/ProcessOutputStreamConsumer.java @@ -0,0 +1,26 @@ +package com.thoughtworks.go.scm.plugin.git.cmd; + +import org.apache.commons.exec.LogOutputStream; + +import java.util.List; + +public class ProcessOutputStreamConsumer extends LogOutputStream { + private final StreamConsumer streamConsumer; + + public ProcessOutputStreamConsumer(StreamConsumer streamConsumer) { + this.streamConsumer = streamConsumer; + } + + public void consumeLine(String line) { + streamConsumer.consumeLine(line); + } + + public List output() { + return streamConsumer.asList(); + } + + @Override + protected void processLine(String line, int level) { + consumeLine(line); + } +} diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/git/cmd/StreamConsumer.java b/src/main/java/com/thoughtworks/go/scm/plugin/git/cmd/StreamConsumer.java new file mode 100644 index 00000000..c1c55b64 --- /dev/null +++ b/src/main/java/com/thoughtworks/go/scm/plugin/git/cmd/StreamConsumer.java @@ -0,0 +1,9 @@ +package com.thoughtworks.go.scm.plugin.git.cmd; + +import java.util.List; + +public interface StreamConsumer { + void consumeLine(String line); + + List asList(); +} diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/CheckoutRequestHandler.java b/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/CheckoutRequestHandler.java index 6389a9dc..5a09a6c9 100644 --- a/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/CheckoutRequestHandler.java +++ b/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/CheckoutRequestHandler.java @@ -3,12 +3,12 @@ import com.thoughtworks.go.plugin.api.logging.Logger; import com.thoughtworks.go.plugin.api.request.GoPluginApiRequest; import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; -import com.thoughtworks.go.scm.plugin.HelperFactory; +import com.thoughtworks.go.scm.plugin.git.GitConfig; +import com.thoughtworks.go.scm.plugin.git.GitHelper; +import com.thoughtworks.go.scm.plugin.git.HelperFactory; +import com.thoughtworks.go.scm.plugin.git.cmd.InMemoryConsumer; +import com.thoughtworks.go.scm.plugin.git.cmd.ProcessOutputStreamConsumer; import com.thoughtworks.go.scm.plugin.util.JsonUtils; -import com.tw.go.plugin.GitHelper; -import com.tw.go.plugin.cmd.InMemoryConsumer; -import com.tw.go.plugin.cmd.ProcessOutputStreamConsumer; -import com.tw.go.plugin.model.GitConfig; import java.io.File; import java.util.ArrayList; @@ -16,7 +16,7 @@ import java.util.Map; public class CheckoutRequestHandler implements RequestHandler { - private static Logger LOGGER = Logger.getLoggerFor(CheckoutRequestHandler.class); + private static final Logger LOGGER = Logger.getLoggerFor(CheckoutRequestHandler.class); @Override @SuppressWarnings("unchecked") diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/FieldValidator.java b/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/FieldValidator.java similarity index 65% rename from src/main/java/com/thoughtworks/go/scm/plugin/FieldValidator.java rename to src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/FieldValidator.java index efb8b07e..933ae704 100644 --- a/src/main/java/com/thoughtworks/go/scm/plugin/FieldValidator.java +++ b/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/FieldValidator.java @@ -1,4 +1,4 @@ -package com.thoughtworks.go.scm.plugin; +package com.thoughtworks.go.scm.plugin.model.requestHandlers; import java.util.Map; diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/GetLatestRevisionRequestHandler.java b/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/GetLatestRevisionRequestHandler.java index e21aa878..0e2d6061 100644 --- a/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/GetLatestRevisionRequestHandler.java +++ b/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/GetLatestRevisionRequestHandler.java @@ -3,12 +3,12 @@ import com.thoughtworks.go.plugin.api.logging.Logger; import com.thoughtworks.go.plugin.api.request.GoPluginApiRequest; import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; -import com.thoughtworks.go.scm.plugin.HelperFactory; +import com.thoughtworks.go.scm.plugin.git.GitConfig; +import com.thoughtworks.go.scm.plugin.git.GitHelper; +import com.thoughtworks.go.scm.plugin.git.HelperFactory; +import com.thoughtworks.go.scm.plugin.git.Revision; import com.thoughtworks.go.scm.plugin.util.JsonUtils; import com.thoughtworks.go.scm.plugin.util.Validator; -import com.tw.go.plugin.GitHelper; -import com.tw.go.plugin.model.GitConfig; -import com.tw.go.plugin.model.Revision; import java.io.File; import java.util.HashMap; @@ -16,7 +16,7 @@ import java.util.Map; public class GetLatestRevisionRequestHandler implements RequestHandler { - private static Logger LOGGER = Logger.getLoggerFor(GetLatestRevisionRequestHandler.class); + private static final Logger LOGGER = Logger.getLoggerFor(GetLatestRevisionRequestHandler.class); @Override @SuppressWarnings("unchecked") diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/LatestRevisionSinceRequestHandler.java b/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/LatestRevisionSinceRequestHandler.java index 5f74fb5d..b1727eee 100644 --- a/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/LatestRevisionSinceRequestHandler.java +++ b/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/LatestRevisionSinceRequestHandler.java @@ -3,12 +3,12 @@ import com.thoughtworks.go.plugin.api.logging.Logger; import com.thoughtworks.go.plugin.api.request.GoPluginApiRequest; import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; -import com.thoughtworks.go.scm.plugin.HelperFactory; +import com.thoughtworks.go.scm.plugin.git.GitConfig; +import com.thoughtworks.go.scm.plugin.git.GitHelper; +import com.thoughtworks.go.scm.plugin.git.HelperFactory; +import com.thoughtworks.go.scm.plugin.git.Revision; import com.thoughtworks.go.scm.plugin.util.JsonUtils; import com.thoughtworks.go.scm.plugin.util.Validator; -import com.tw.go.plugin.GitHelper; -import com.tw.go.plugin.model.GitConfig; -import com.tw.go.plugin.model.Revision; import java.io.File; import java.util.HashMap; @@ -18,7 +18,7 @@ import static java.util.stream.Collectors.toList; public class LatestRevisionSinceRequestHandler implements RequestHandler { - private static Logger LOGGER = Logger.getLoggerFor(LatestRevisionSinceRequestHandler.class); + private static final Logger LOGGER = Logger.getLoggerFor(LatestRevisionSinceRequestHandler.class); @Override @SuppressWarnings("unchecked") diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/RevisionUtil.java b/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/RevisionUtil.java index ef3b791d..3e2d9098 100644 --- a/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/RevisionUtil.java +++ b/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/RevisionUtil.java @@ -1,10 +1,12 @@ package com.thoughtworks.go.scm.plugin.model.requestHandlers; -import com.tw.go.plugin.model.Revision; +import com.thoughtworks.go.scm.plugin.git.Revision; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; -import java.util.*; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; public class RevisionUtil { diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/SCMCheckConnectionRequestHandler.java b/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/SCMCheckConnectionRequestHandler.java index 66a9c2e3..e0e4b976 100644 --- a/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/SCMCheckConnectionRequestHandler.java +++ b/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/SCMCheckConnectionRequestHandler.java @@ -3,12 +3,12 @@ import com.thoughtworks.go.plugin.api.logging.Logger; import com.thoughtworks.go.plugin.api.request.GoPluginApiRequest; import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; -import com.thoughtworks.go.scm.plugin.HelperFactory; +import com.thoughtworks.go.scm.plugin.git.GitConfig; +import com.thoughtworks.go.scm.plugin.git.GitHelper; +import com.thoughtworks.go.scm.plugin.git.HelperFactory; import com.thoughtworks.go.scm.plugin.util.JsonUtils; +import com.thoughtworks.go.scm.plugin.util.StringUtil; import com.thoughtworks.go.scm.plugin.util.Validator; -import com.tw.go.plugin.GitHelper; -import com.tw.go.plugin.model.GitConfig; -import com.tw.go.plugin.util.StringUtil; import java.io.File; import java.util.ArrayList; @@ -18,7 +18,7 @@ import static com.thoughtworks.go.scm.plugin.util.Validator.isValidURL; public class SCMCheckConnectionRequestHandler implements RequestHandler { - private static Logger LOGGER = Logger.getLoggerFor(SCMCheckConnectionRequestHandler.class); + private static final Logger LOGGER = Logger.getLoggerFor(SCMCheckConnectionRequestHandler.class); @Override public GoPluginApiResponse handle(GoPluginApiRequest goPluginApiRequest) { @@ -40,7 +40,7 @@ public GoPluginApiResponse handle(GoPluginApiRequest goPluginApiRequest) { private void checkConnection(GitConfig gitConfig, Map response, ArrayList messages) { LOGGER.debug("SCMCheckConnectionRequestHandler In handle"); try { - if (StringUtil.isEmpty(gitConfig.getUrl())) { + if (StringUtil.isBlank(gitConfig.getUrl())) { response.put("status", "failure"); messages.add("URL is empty"); } else if (gitConfig.getUrl().startsWith("/")) { @@ -48,7 +48,7 @@ private void checkConnection(GitConfig gitConfig, Map response, response.put("status", "failure"); messages.add("Could not find Git repository"); } else { - GitHelper gitHelper = HelperFactory.git(gitConfig, null); + GitHelper gitHelper = new GitHelper(gitConfig, null); gitHelper.checkConnection(); } } else { diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/SCMValidationRequestHandler.java b/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/SCMValidationRequestHandler.java index 4296f67c..751035a7 100644 --- a/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/SCMValidationRequestHandler.java +++ b/src/main/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/SCMValidationRequestHandler.java @@ -2,10 +2,9 @@ import com.thoughtworks.go.plugin.api.request.GoPluginApiRequest; import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; -import com.thoughtworks.go.scm.plugin.FieldValidator; +import com.thoughtworks.go.scm.plugin.git.GitConfig; import com.thoughtworks.go.scm.plugin.util.JsonUtils; import com.thoughtworks.go.scm.plugin.util.Validator; -import com.tw.go.plugin.model.GitConfig; import java.util.ArrayList; import java.util.HashMap; diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/util/DateUtils.java b/src/main/java/com/thoughtworks/go/scm/plugin/util/DateUtils.java new file mode 100644 index 00000000..39633175 --- /dev/null +++ b/src/main/java/com/thoughtworks/go/scm/plugin/util/DateUtils.java @@ -0,0 +1,35 @@ +package com.thoughtworks.go.scm.plugin.util; + +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + +public class DateUtils { + private static final DateTimeFormatter formatter = ISODateTimeFormat.dateTimeNoMillis(); + + public static Date parseISO8601(String date) { + try { + DateTime dateTime = formatter.parseDateTime(date); + return dateTime.toDate(); + } catch (Exception e) { + //fall through and try and parse other ISO standard formats + } + try { + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss ZZZZZ").parse(date); + } catch (ParseException e) { + //fall through and try and parse other ISO standard formats + } + try { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + return dateFormat.parse(date); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/util/JsonUtils.java b/src/main/java/com/thoughtworks/go/scm/plugin/util/JsonUtils.java index d2c4cc4b..444ca452 100644 --- a/src/main/java/com/thoughtworks/go/scm/plugin/util/JsonUtils.java +++ b/src/main/java/com/thoughtworks/go/scm/plugin/util/JsonUtils.java @@ -4,9 +4,9 @@ import com.thoughtworks.go.plugin.api.logging.Logger; import com.thoughtworks.go.plugin.api.request.GoPluginApiRequest; import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; +import com.thoughtworks.go.scm.plugin.git.GitConfig; +import com.thoughtworks.go.scm.plugin.git.ShallowClone; import com.thoughtworks.go.scm.plugin.model.requestHandlers.SCMConfigurationRequestHandler; -import com.tw.go.plugin.model.GitConfig; -import com.tw.go.plugin.model.ShallowClone; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/util/StringUtil.java b/src/main/java/com/thoughtworks/go/scm/plugin/util/StringUtil.java new file mode 100644 index 00000000..30976f47 --- /dev/null +++ b/src/main/java/com/thoughtworks/go/scm/plugin/util/StringUtil.java @@ -0,0 +1,7 @@ +package com.thoughtworks.go.scm.plugin.util; + +public class StringUtil { + public static boolean isBlank(String str) { + return str == null || str.isBlank(); + } +} diff --git a/src/main/java/com/thoughtworks/go/scm/plugin/util/Validator.java b/src/main/java/com/thoughtworks/go/scm/plugin/util/Validator.java index 35165cb9..97252578 100644 --- a/src/main/java/com/thoughtworks/go/scm/plugin/util/Validator.java +++ b/src/main/java/com/thoughtworks/go/scm/plugin/util/Validator.java @@ -1,7 +1,6 @@ package com.thoughtworks.go.scm.plugin.util; -import com.tw.go.plugin.model.GitConfig; -import com.tw.go.plugin.util.StringUtil; +import com.thoughtworks.go.scm.plugin.git.GitConfig; import java.io.File; import java.util.Map; @@ -17,7 +16,7 @@ public static boolean isValidURL(String url) { } public static void validateUrl(GitConfig gitConfig, Map fieldMap) { - if (StringUtil.isEmpty(gitConfig.getUrl())) { + if (StringUtil.isBlank(gitConfig.getUrl())) { fieldMap.put("key", "url"); fieldMap.put("message", "URL is a required field"); } else { diff --git a/src/test/java/com/thoughtworks/go/scm/plugin/git/GitConfigTest.java b/src/test/java/com/thoughtworks/go/scm/plugin/git/GitConfigTest.java new file mode 100644 index 00000000..d6fc264c --- /dev/null +++ b/src/test/java/com/thoughtworks/go/scm/plugin/git/GitConfigTest.java @@ -0,0 +1,144 @@ +package com.thoughtworks.go.scm.plugin.git; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + + +public class GitConfigTest { + @Test + public void shouldGetEffectiveBranch() { + assertThat(new GitConfig("url", null, null, null).getEffectiveBranch()).isEqualTo("master"); + assertThat(new GitConfig("url", null, null, "branch").getEffectiveBranch()).isEqualTo("branch"); + } + + @Test + public void isRemoteUrlShouldBeTrueForHttp() { + GitConfig gitConfig = new GitConfig("http://url.test", "user", "password", "master"); + + assertThat(gitConfig.isRemoteUrl()).isEqualTo(Boolean.TRUE); + } + + @Test + public void isRemoteUrlShouldBeTrueForHttps() { + GitConfig gitConfig = new GitConfig("https://url.test", "user", "password", "master"); + + assertThat(gitConfig.isRemoteUrl()).isEqualTo(Boolean.TRUE); + } + + @Test + public void isRemoteUrlShouldBeTrueForGitUrl() { + GitConfig gitConfig = new GitConfig("git@github.com:test/sample.git", "user", "password", "master"); + + assertThat(gitConfig.isRemoteUrl()).isEqualTo(Boolean.FALSE); + } + + @Test + public void hasCredentialsShouldBeTrueIfUrlAndPasswordAreProvided() { + GitConfig gitConfig = new GitConfig("https://url.test", "user", "password", "master"); + + assertThat(gitConfig.hasCredentials()).isEqualTo(Boolean.TRUE); + } + + @Test + public void hasCredentialsShouldBeFalseIfUrlIsEmptyWithValidPassword() { + GitConfig gitConfig = new GitConfig("", "user", "password", "master"); + + assertThat(gitConfig.hasCredentials()).isEqualTo(Boolean.FALSE); + } + + @Test + public void hasCredentialsShouldBeFalseWithValidUrlAndEmptyPassword() { + GitConfig gitConfig = new GitConfig("http://url.test", "user", "", "master"); + + assertThat(gitConfig.hasCredentials()).isEqualTo(Boolean.FALSE); + } + + @Test + public void shouldGetEffectiveUrl() { + assertThat(new GitConfig("/tmp/git-repo", null, null, null).getEffectiveUrl()).isEqualTo("/tmp/git-repo"); + assertThat(new GitConfig("/tmp/git-repo", "username", "password", null).getEffectiveUrl()).isEqualTo("/tmp/git-repo"); + assertThat(new GitConfig("http://github.com/gocd/gocd", null, null, null).getEffectiveUrl()).isEqualTo("http://github.com/gocd/gocd"); + assertThat(new GitConfig("http://github.com/gocd/gocd", "username", "password", null).getEffectiveUrl()).isEqualTo("http://username:password@github.com/gocd/gocd"); + assertThat(new GitConfig("https://github.com/gocd/gocd", "username", "password", null).getEffectiveUrl()).isEqualTo("https://username:password@github.com/gocd/gocd"); + } + + @Test + public void getEffectiveUrlShouldContainUserNameAndPasswordForRemoteUrlWithValidCredential() { + GitConfig gitConfig = new GitConfig("http://url.test", "user", "password", "master"); + + String effectiveUrl = gitConfig.getEffectiveUrl(); + assertThat(effectiveUrl).isEqualTo("http://user:password@url.test"); + } + + @Test + public void getEffectiveUrlShouldNotContainUserNameAndPasswordForNonRemoteUrlWithValidCredential() { + GitConfig gitConfig = new GitConfig("git@github.com:test/sample.git", "user", "password", "master"); + + String effectiveUrl = gitConfig.getEffectiveUrl(); + assertThat(effectiveUrl).isEqualTo("git@github.com:test/sample.git"); + } + + @Test + public void getEffectiveUrlShouldNotContainUserNameAndPasswordForRemoteUrlWithoutCredential() { + GitConfig gitConfig = new GitConfig("http://url.test", "user", "", "master"); + + String effectiveUrl = gitConfig.getEffectiveUrl(); + assertThat(effectiveUrl).isEqualTo("http://url.test"); + } + + @Test + public void getEffectiveBranchShouldReturnTheSpecifiedBranch() { + GitConfig gitConfig = new GitConfig("http://url.test", "user", "password", "staging"); + + String effectiveBranch = gitConfig.getEffectiveBranch(); + + assertThat(effectiveBranch).isEqualTo("staging"); + } + + @Test + public void getEffectiveBranchShouldReturnMasterIfBranchisNotSpecified() { + GitConfig gitConfig = new GitConfig("http://url.test", "user", "password", ""); + + String effectiveBranch = gitConfig.getEffectiveBranch(); + + assertThat(effectiveBranch).isEqualTo("master"); + } + + @Test + public void shouldBeAbleToGetIsRecursiveSubModuleUpdate() { + GitConfig gitConfig = new GitConfig("http://url.test", "username", "password", "branch"); + + boolean recursiveSubModuleUpdate = gitConfig.isRecursiveSubModuleUpdate(); + + assertThat(recursiveSubModuleUpdate).isEqualTo(Boolean.TRUE); + } + + @Test + public void shouldGetUrl() { + GitConfig gitConfig = new GitConfig("http://url.test", "username", "password", "branch"); + + assertThat(gitConfig.getUrl()).isEqualTo("http://url.test"); + } + + @Test + public void shouldGetUsername() { + GitConfig gitConfig = new GitConfig("http://url.test", "username", "password", "branch"); + + assertThat(gitConfig.getUsername()).isEqualTo("username"); + } + + @Test + public void getPassword() { + GitConfig gitConfig = new GitConfig("http://url.test", "username", "password", "branch"); + + assertThat(gitConfig.getPassword()).isEqualTo("password"); + } + + @Test + public void getBranch() { + GitConfig gitConfig = new GitConfig("http://url.test", "username", "password", "branch"); + + assertThat(gitConfig.getBranch()).isEqualTo("branch"); + } +} diff --git a/src/test/java/com/thoughtworks/go/scm/plugin/git/GitHelperTest.java b/src/test/java/com/thoughtworks/go/scm/plugin/git/GitHelperTest.java new file mode 100644 index 00000000..df7308db --- /dev/null +++ b/src/test/java/com/thoughtworks/go/scm/plugin/git/GitHelperTest.java @@ -0,0 +1,439 @@ +package com.thoughtworks.go.scm.plugin.git; + +import com.thoughtworks.go.scm.plugin.git.cmd.InMemoryConsumer; +import com.thoughtworks.go.scm.plugin.git.cmd.ProcessOutputStreamConsumer; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static uk.org.webcompere.systemstubs.SystemStubs.restoreSystemProperties; + +public class GitHelperTest { + private static final int BUFFER_SIZE = 4096; + protected final File testRepository = new File(System.getProperty("java.io.tmpdir"), UUID.randomUUID().toString()); + protected final File simpleGitRepository = new File(System.getProperty("java.io.tmpdir"), "simple-git-repository"); + private final File subModuleGitRepository = new File(System.getProperty("java.io.tmpdir"), "sub-module-git-repository"); + private final File branchGitRepository = new File(System.getProperty("java.io.tmpdir"), "branch-git-repository"); + private final File mergeCommitGitRepository = new File(System.getProperty("java.io.tmpdir"), "merge-commit-git-repository"); + + protected GitHelper getHelper(GitConfig gitConfig, File workingDir) { + ProcessOutputStreamConsumer stdOut = new ProcessOutputStreamConsumer(new InMemoryConsumer() { + @Override + public void consumeLine(String line) { + System.out.println(line); + } + }); + return new GitHelper(gitConfig, workingDir, stdOut, new ProcessOutputStreamConsumer(new InMemoryConsumer() { + @Override + public void consumeLine(String line) { + System.err.println(line); + } + })); + } + + @BeforeEach + public void setUp() { + cleanTmpFiles(); + } + + @AfterEach + public void tearDown() { + cleanTmpFiles(); + } + + private void cleanTmpFiles() { + FileUtils.deleteQuietly(testRepository); + FileUtils.deleteQuietly(simpleGitRepository); + FileUtils.deleteQuietly(subModuleGitRepository); + FileUtils.deleteQuietly(branchGitRepository); + FileUtils.deleteQuietly(mergeCommitGitRepository); + } + + @Test + public void shouldGetVersion() { + GitHelper git = getHelper(null, null); + assertThat(git.version()).isNotNull(); + } + + @Test + public void shouldCheckConnection() throws Exception { + extractToTmp("/sample-repository/simple-git-repository-1.zip"); + + GitHelper gitValidRepository = getHelper(new GitConfig(simpleGitRepository.getAbsolutePath()), null); + try { + gitValidRepository.checkConnection(); + } catch (Throwable t) { + fail("check connection failed for a valid repository"); + } + + GitHelper gitInValidRepository = getHelper(new GitConfig(new File(System.getProperty("java.io.tmpdir"), "non-existing-repository").getAbsolutePath()), null); + try { + gitInValidRepository.checkConnection(); + fail("check connection failed for a valid repository"); + } catch (Exception e) { + assertThat(e).isInstanceOf(RuntimeException.class); + } + } + + @Test + public void shouldGetRevisionForRepository() throws Exception { + extractToTmp("/sample-repository/simple-git-repository-1.zip"); + + GitHelper git = getHelper(new GitConfig(simpleGitRepository.getAbsolutePath()), testRepository); + git.cloneOrFetch(); + + assertThat(git.workingRepositoryUrl()).isEqualTo(simpleGitRepository.getAbsolutePath()); + assertThat(git.getCommitCount()).isEqualTo(1); + assertThat(git.currentRevision()).isEqualTo("012e893acea10b140688d11beaa728e8c60bd9f6"); + + Revision revision = git.getDetailsForRevision("012e893acea10b140688d11beaa728e8c60bd9f6"); + verifyRevision(revision, "012e893acea10b140688d11beaa728e8c60bd9f6", "1", 1422184635000L, List.of(Pair.of("a.txt", "added"))); + } + + @Test + public void shouldPollRepository() throws Exception { + // Checkout & Get LatestRevision + extractToTmp("/sample-repository/simple-git-repository-1.zip"); + + GitHelper git = getHelper(new GitConfig(simpleGitRepository.getAbsolutePath()), testRepository); + git.cloneOrFetch(); + + assertThat(git.getCurrentBranch()).isEqualTo("master"); + assertThat(git.getCommitCount()).isEqualTo(1); + + Revision revision = git.getLatestRevision(); + + verifyRevision(revision, "012e893acea10b140688d11beaa728e8c60bd9f6", "1", 1422184635000L, List.of(Pair.of("a.txt", "added"))); + + // Fetch & Get LatestRevisionsSince + FileUtils.deleteQuietly(simpleGitRepository.getAbsoluteFile()); + extractToTmp("/sample-repository/simple-git-repository-2.zip"); + + git.cloneOrFetch(); + + assertThat(git.getCurrentBranch()).isEqualTo("master"); + assertThat(git.getCommitCount()).isEqualTo(3); + + List newerRevisions = git.getRevisionsSince("012e893acea10b140688d11beaa728e8c60bd9f6"); + + assertThat(newerRevisions.size()).isEqualTo(2); + verifyRevision(newerRevisions.get(0), "24ce45d1a1427b643ae859777417bbc9f0d7cec8", "3\ntest multiline\ncomment", 1422189618000L, List.of(Pair.of("a.txt", "modified"), Pair.of("b.txt", "added"))); + verifyRevision(newerRevisions.get(1), "1320a78055558603a2c29d803bbaa50d3542ff50", "2", 1422189545000L, List.of(Pair.of("a.txt", "modified"))); + + // poll again + git.cloneOrFetch(); + + newerRevisions = git.getRevisionsSince("24ce45d1a1427b643ae859777417bbc9f0d7cec8"); + + assertThat(newerRevisions.isEmpty()).isEqualTo(true); + } + + @Test + public void shouldGetLatestRevisionForSubpaths() throws Exception { + extractToTmp("/sample-repository/simple-git-repository-3.zip"); + + GitHelper git = getHelper(new GitConfig(simpleGitRepository.getAbsolutePath()), testRepository); + git.cloneOrFetch(); + + assertThat(git.getCommitCount()).isEqualTo(4); + + final Revision aRevision = git.getLatestRevision(List.of("a.txt")); + final Revision bRevision = git.getLatestRevision(List.of("b.txt")); + + assertThat(aRevision.getRevision()).isEqualTo("7d14db6ec07f2cfac82195e401780bf127349ddb"); + assertThat(aRevision.getModifiedFiles()).hasSize(1); + verifyRevision(aRevision, "7d14db6ec07f2cfac82195e401780bf127349ddb", "Change to a.txt", 1567878351000L, List.of(Pair.of("a.txt", "modified"))); + + assertThat(bRevision.getRevision()).isEqualTo("24ce45d1a1427b643ae859777417bbc9f0d7cec8"); + assertThat(bRevision.getModifiedFiles()).hasSize(2); + + final Revision eitherRevision = git.getLatestRevision(List.of("a.txt", "b.txt")); + assertThat(eitherRevision.getRevision()).isEqualTo(aRevision.getRevision()); + + List aRevisions = git.getRevisionsSince("24ce45d1a1427b643ae859777417bbc9f0d7cec8", List.of("a.txt")); + assertThat(aRevisions).hasSize(1); + verifyRevision(aRevisions.get(0), "7d14db6ec07f2cfac82195e401780bf127349ddb", "Change to a.txt", 1567878351000L, List.of(Pair.of("a.txt", "modified"))); + + List bRevisions = git.getRevisionsSince("24ce45d1a1427b643ae859777417bbc9f0d7cec8", List.of("b.txt")); + assertThat(bRevisions).hasSize(0); + + List eitherRevisions = git.getRevisionsSince("012e893acea10b140688d11beaa728e8c60bd9f6", List.of("a.txt","b.txt")); + assertThat(eitherRevisions).hasSize(3); + assertThat(eitherRevisions.get(2).getRevision()).isEqualTo("1320a78055558603a2c29d803bbaa50d3542ff50"); + } + + @Test + public void shouldCheckoutBranch() throws Exception { + extractToTmp("/sample-repository/branch-git-repository.zip"); + + GitHelper git = getHelper(new GitConfig(branchGitRepository.getAbsolutePath(), null, null, "feature-branch"), testRepository); + git.cloneOrFetch(); + + assertThat(git.getCurrentBranch()).isEqualTo("feature-branch"); + assertThat(git.getCommitCount()).isEqualTo(2); + assertThat(new File(testRepository, "a.txt").exists()).isEqualTo(true); + assertThat(new File(testRepository, "b.txt").exists()).isEqualTo(true); + } + + @Test + public void shouldGetBranchToRevisionMap() throws Exception { + extractToTmp("/sample-repository/branch-git-repository.zip"); + + GitHelper git = getHelper(new GitConfig(branchGitRepository.getAbsolutePath(), null, null, null), testRepository); + git.cloneOrFetch(); + + Map branchToRevisionMap = git.getBranchToRevisionMap(); + + assertThat(branchToRevisionMap.size()).isEqualTo(2); + assertThat(branchToRevisionMap.get("master")).isEqualTo("012e893acea10b140688d11beaa728e8c60bd9f6"); + assertThat(branchToRevisionMap.get("feature-branch")).isEqualTo("765e24764ee4f6fc10e4301b4f9528c08ff178d4"); + } + + @Test + public void shouldRecursiveSubModuleUpdate() throws Exception { + restoreSystemProperties(() -> { + System.setProperty(GitHelper.GIT_SUBMODULE_ALLOW_FILE_PROTOCOL, "Y"); + extractToTmp("/sample-repository/simple-git-repository-1.zip"); + extractToTmp("/sample-repository/sub-module-git-repository.zip"); + + GitHelper gitRemote = getHelper(new GitConfig(simpleGitRepository.getAbsolutePath()), simpleGitRepository); + gitRemote.submoduleAdd(subModuleGitRepository.getAbsolutePath(), "sub-module", "sub-module"); + gitRemote.commit("add sub-module"); + + GitConfig gitConfig = new GitConfig(simpleGitRepository.getAbsolutePath(), null, null, "master", true, false); + GitHelper gitMain = getHelper(gitConfig, testRepository); + gitMain.cloneOrFetch(); + + assertThat(gitMain.getCommitCount()).isEqualTo(2); + + assertThat(gitMain.getSubModuleCommitCount("sub-module")).isEqualTo(2); + + // TODO: add commit to sub-module & main-repo + + // poll again + gitMain.cloneOrFetch(); + + assertThat(gitMain.getCommitCount()).isEqualTo(2); + + assertThat(gitMain.getSubModuleCommitCount("sub-module")).isEqualTo(2); + }); + } + + @Test + public void shouldWorkWithRepositoriesWithSubModules() throws Exception { + restoreSystemProperties(() -> { + System.setProperty(GitHelper.GIT_SUBMODULE_ALLOW_FILE_PROTOCOL, "Y"); + extractToTmp("/sample-repository/simple-git-repository-1.zip"); + extractToTmp("/sample-repository/sub-module-git-repository.zip"); + + GitHelper gitRemote = getHelper(new GitConfig(simpleGitRepository.getAbsolutePath()), simpleGitRepository); + gitRemote.submoduleAdd(subModuleGitRepository.getAbsolutePath(), "sub-module", "sub-module"); + gitRemote.commit("add sub-module"); + + List submoduleFolders = gitRemote.submoduleFolders(); + assertThat(submoduleFolders.size()).isEqualTo(1); + assertThat(submoduleFolders.get(0)).isEqualTo("sub-module"); + }); + } + + @Test + public void shouldCheckoutToRevision() throws Exception { + extractToTmp("/sample-repository/simple-git-repository-2.zip"); + + GitHelper git = getHelper(new GitConfig(simpleGitRepository.getAbsolutePath()), testRepository); + git.cloneOrFetch(); + + git.resetHard("24ce45d1a1427b643ae859777417bbc9f0d7cec8"); + + assertThat(new File(testRepository, "a.txt").exists()).isEqualTo(true); + assertThat(new File(testRepository, "b.txt").exists()).isEqualTo(true); + + git.resetHard("1320a78055558603a2c29d803bbaa50d3542ff50"); + + assertThat(new File(testRepository, "a.txt").exists()).isEqualTo(true); + assertThat(new File(testRepository, "b.txt").exists()).isEqualTo(false); + } + + @Test + public void shouldInitAddCommit() throws Exception { + testRepository.mkdirs(); + + GitHelper git = getHelper(null, testRepository); + git.init(); + File file = new File(testRepository, "a.txt"); + FileUtils.writeStringToFile(file, "content", StandardCharsets.UTF_8); + git.add(file); + git.commit("comment"); + + List allRevisions = git.getAllRevisions(); + assertThat(allRevisions.size()).isEqualTo(1); + + Revision revision = allRevisions.get(0); + assertThat(revision.getComment()).isEqualTo("comment"); + assertThat(revision.getModifiedFiles().size()).isEqualTo(1); + assertThat(revision.getModifiedFiles().get(0).getFileName()).isEqualTo("a.txt"); + assertThat(revision.getModifiedFiles().get(0).getAction()).isEqualTo("added"); + } + + @Test @Disabled + public void shouldWorkWithGithubRepository() { + GitHelper git = getHelper(new GitConfig("https://github.com/mdaliejaz/samplerepo.git"), testRepository); + git.cloneOrFetch("+refs/pull/*/merge:refs/gh-merge/remotes/origin/*"); + + Map branchToRevisionMap = git.getBranchToRevisionMap("refs/gh-merge/remotes/origin/"); + + assertThat(branchToRevisionMap.size()).isEqualTo(1); + assertThat(branchToRevisionMap.get("1")).isEqualTo("aabd0f242bd40bfaaa4ce359123b2a2d976077d1"); + } + + @Test + public void shouldReturnModifiedFilesForMergeCommit() throws Exception { + extractToTmp("/sample-repository/merge-commit-git-repository.zip"); + + GitHelper git = getHelper(new GitConfig(mergeCommitGitRepository.getAbsolutePath()), mergeCommitGitRepository); + Revision revision = git.getDetailsForRevision("66a1b17514622a8e4a620a033cca3715ef870e71"); + + verifyRevision(revision, "66a1b17514622a8e4a620a033cca3715ef870e71", "Merge branch 'master' into test-branch", 1477248891000L, List.of(Pair.of("file.txt", "modified"))); + assertTrue(revision.isMergeCommit(), "Revision should be a merge commit"); + } + + + @Test + public void shouldShallowClone() throws Exception { + extractToTmp("/sample-repository/simple-git-repository-2.zip"); + GitConfig config = new GitConfig("file://" + simpleGitRepository.getAbsolutePath()); + config.setShallowClone(new ShallowClone(1, 2)); + GitHelper git = getHelper(config, testRepository); + + git.cloneOrFetch(); + + assertThat(git.getCommitCount()).isEqualTo(1); + + Revision revision = git.getLatestRevision(); + verifyRevision(revision, "24ce45d1a1427b643ae859777417bbc9f0d7cec8", "3\ntest multiline\ncomment", 1422189618000L, List.of(Pair.of("a.txt", "added"), Pair.of("b.txt", "added"))); + List newerRevisions = git.getRevisionsSince("24ce45d1a1427b643ae859777417bbc9f0d7cec8"); + assertThat(newerRevisions.isEmpty()).isEqualTo(true); + + FileUtils.deleteQuietly(testRepository); + + // Increase default depth + config.setShallowClone(new ShallowClone(2, 3)); + // poll again + git.cloneOrFetch(); + + assertThat(git.getCommitCount()).isEqualTo(2); + verifyRevision(revision, "24ce45d1a1427b643ae859777417bbc9f0d7cec8", "3\ntest multiline\ncomment", 1422189618000L, List.of(Pair.of("a.txt", "added"), Pair.of("b.txt", "added"))); + } + + @Test + public void shallowCloneShouldFetchMoreCommitsOnResetIfNecessary() throws Exception { + extractToTmp("/sample-repository/simple-git-repository-2.zip"); + GitConfig config = new GitConfig("file://" + simpleGitRepository.getAbsolutePath()); + config.setShallowClone(new ShallowClone(1, 2)); + GitHelper git = getHelper(config, testRepository); + + git.cloneOrFetch(); + + assertThat(git.getCommitCount()).isEqualTo(1); + + Revision revision = git.getLatestRevision(); + assertThat(revision.getRevision()).isEqualTo("24ce45d1a1427b643ae859777417bbc9f0d7cec8"); + + git.resetHard("012e893acea10b140688d11beaa728e8c60bd9f6"); + + assertThat(git.getCommitCount()).isEqualTo(1); + } + + @Test + public void shouldCloneWithNoCheckout() throws Exception { + extractToTmp("/sample-repository/simple-git-repository-2.zip"); + + GitConfig config = new GitConfig("file://" + simpleGitRepository.getAbsolutePath()); + config.setNoCheckout(true); + GitHelper git = getHelper(config, testRepository); + + git.cloneOrFetch(); + assertThat(List.of(testRepository.list())).contains(".git"); + + assertThat(git.getCommitCount()).isEqualTo(3); + + Revision revision = git.getLatestRevision(); + verifyRevision(revision, "24ce45d1a1427b643ae859777417bbc9f0d7cec8", "3\ntest multiline\ncomment", 1422189618000L, List.of(Pair.of("a.txt", "modified"), Pair.of("b.txt", "added"))); + + // poll again + git.cloneOrFetch(); + assertThat(List.of(testRepository.list())).contains(".git"); + + List newerRevisions = git.getRevisionsSince("24ce45d1a1427b643ae859777417bbc9f0d7cec8"); + + assertThat(newerRevisions.isEmpty()).isEqualTo(true); + } + + protected void extractToTmp(String zipResourcePath) throws IOException { + File zipFile = new File(System.getProperty("java.io.tmpdir"), UUID.randomUUID().toString() + ".zip"); + + IOUtils.copy(getClass().getResourceAsStream(zipResourcePath), new FileOutputStream(zipFile)); + + unzip(zipFile.getAbsolutePath(), System.getProperty("java.io.tmpdir")); + + FileUtils.deleteQuietly(zipFile); + } + + private void unzip(String zipFilePath, String destinationDirectoryPath) throws IOException { + File destinationDirectory = new File(destinationDirectoryPath); + if (!destinationDirectory.exists()) { + FileUtils.forceMkdir(destinationDirectory); + } + + ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(zipFilePath)); + ZipEntry entry = zipInputStream.getNextEntry(); + while (entry != null) { + String filePath = destinationDirectoryPath + File.separator + entry.getName(); + if (!entry.isDirectory()) { + extractFile(zipInputStream, filePath); + } else { + FileUtils.forceMkdir(new File(filePath)); + } + + zipInputStream.closeEntry(); + entry = zipInputStream.getNextEntry(); + } + zipInputStream.close(); + } + + private void extractFile(ZipInputStream zipInputStream, String filePath) throws IOException { + BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(filePath)); + byte[] bytesRead = new byte[BUFFER_SIZE]; + int readByteCount; + while ((readByteCount = zipInputStream.read(bytesRead)) != -1) { + bufferedOutputStream.write(bytesRead, 0, readByteCount); + } + bufferedOutputStream.close(); + } + + protected void verifyRevision(Revision revision, String sha, String comment, long timestamp, List> files) { + assertThat(revision.getRevision()).isEqualTo(sha); + assertThat(revision.getTimestamp().getTime()).isEqualTo(timestamp); + assertThat(revision.getComment()).isEqualTo(comment); + assertThat(revision.getModifiedFiles().size()).isEqualTo(files.size()); + for (int i = 0; i < files.size(); i++) { + assertThat(revision.getModifiedFiles().get(i).getFileName()).isEqualTo(files.get(i).getLeft()); + assertThat(revision.getModifiedFiles().get(i).getAction()).isEqualTo(files.get(i).getRight()); + } + } +} diff --git a/src/test/java/com/thoughtworks/go/scm/plugin/git/RevisionTest.java b/src/test/java/com/thoughtworks/go/scm/plugin/git/RevisionTest.java new file mode 100644 index 00000000..22475a54 --- /dev/null +++ b/src/test/java/com/thoughtworks/go/scm/plugin/git/RevisionTest.java @@ -0,0 +1,53 @@ +package com.thoughtworks.go.scm.plugin.git; + +import org.joda.time.DateTime; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RevisionTest { + private Revision revision; + private Date date; + + @BeforeEach + public void setUp() { + date = DateTime.now().toDate(); + revision = new Revision("revision", date, "comments", "user", "email", null); + } + + @Test + public void createModifiedFile() { + revision.createModifiedFile("fileName", "added"); + + assertThat(revision.getModifiedFiles()).hasSize(1); + assertThat(revision.getModifiedFiles()).contains(new ModifiedFile("fileName", "added")); + } + + @Test + public void getRevision() { + assertThat(revision.getRevision()).isEqualTo("revision"); + } + + @Test + public void getTimestamp() { + assertThat(revision.getTimestamp()).isEqualTo(date); + } + + @Test + public void getComment() { + assertThat(revision.getComment()).isEqualTo("comments"); + } + + @Test + public void getUser() { + assertThat(revision.getUser()).isEqualTo("user"); + } + + @Test + public void getEmailId() { + assertThat(revision.getEmailId()).isEqualTo("email"); + } +} \ No newline at end of file diff --git a/src/test/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/CheckoutRequestHandlerTest.java b/src/test/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/CheckoutRequestHandlerTest.java index bbe4ed07..9f85e733 100644 --- a/src/test/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/CheckoutRequestHandlerTest.java +++ b/src/test/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/CheckoutRequestHandlerTest.java @@ -2,12 +2,11 @@ import com.thoughtworks.go.plugin.api.request.GoPluginApiRequest; import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; -import com.thoughtworks.go.scm.plugin.HelperFactory; +import com.thoughtworks.go.scm.plugin.git.GitConfig; +import com.thoughtworks.go.scm.plugin.git.GitHelper; +import com.thoughtworks.go.scm.plugin.git.HelperFactory; +import com.thoughtworks.go.scm.plugin.git.cmd.ProcessOutputStreamConsumer; import com.thoughtworks.go.scm.plugin.util.JsonUtils; -import com.tw.go.plugin.GitHelper; -import com.tw.go.plugin.cmd.ProcessOutputStreamConsumer; -import com.tw.go.plugin.model.GitConfig; -import org.eclipse.jgit.errors.TransportException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -35,7 +34,7 @@ public class CheckoutRequestHandlerTest { @Mock private GitConfig gitConfigMock; - private String revision = "b6d7a9c"; + private final String revision = "b6d7a9c"; private final String destinationFolder = "destination"; @Test @@ -71,7 +70,7 @@ public void shouldHandleApiRequestAndRenderErrorApiResponse() { RequestHandler checkoutRequestHandler = new CheckoutRequestHandler(); ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(Throwable.class); - TransportException cause = new TransportException("git@github.com:lifealike/gocd-config.git: UnknownHostKey: github.com. RSA key fingerprint is 16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48"); + Exception cause = new IllegalArgumentException("git@github.com:lifealike/gocd-config.git: UnknownHostKey: github.com. RSA key fingerprint is 16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48"); RuntimeException runtimeException = new RuntimeException("clone failed", cause); doThrow(runtimeException).when(gitHelperMock).cloneOrFetch(); when(JsonUtils.renderErrorApiResponse(eq(pluginApiRequestMock), errorCaptor.capture())).thenReturn(mock(GoPluginApiResponse.class)); diff --git a/src/test/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/GetLatestRevisionRequestHandlerTest.java b/src/test/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/GetLatestRevisionRequestHandlerTest.java index 673625cd..41ad68cc 100644 --- a/src/test/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/GetLatestRevisionRequestHandlerTest.java +++ b/src/test/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/GetLatestRevisionRequestHandlerTest.java @@ -2,12 +2,11 @@ import com.thoughtworks.go.plugin.api.request.GoPluginApiRequest; import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; -import com.thoughtworks.go.scm.plugin.HelperFactory; +import com.thoughtworks.go.scm.plugin.git.GitConfig; +import com.thoughtworks.go.scm.plugin.git.GitHelper; +import com.thoughtworks.go.scm.plugin.git.HelperFactory; +import com.thoughtworks.go.scm.plugin.git.Revision; import com.thoughtworks.go.scm.plugin.util.JsonUtils; -import com.tw.go.plugin.GitHelper; -import com.tw.go.plugin.model.GitConfig; -import com.tw.go.plugin.model.Revision; -import org.eclipse.jgit.errors.TransportException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -95,7 +94,7 @@ public void shouldHandleApiRequestAndRenderErrorApiResponseWhenCloneFailed() { RequestHandler checkoutRequestHandler = new GetLatestRevisionRequestHandler(); ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(Throwable.class); - TransportException cause = new TransportException("git@github.com:lifealike/gocd-config.git: UnknownHostKey: github.com. RSA key fingerprint is 16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48"); + Exception cause = new IllegalArgumentException("git@github.com:lifealike/gocd-config.git: UnknownHostKey: github.com. RSA key fingerprint is 16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48"); RuntimeException runtimeException = new RuntimeException("clone failed", cause); when(JsonUtils.renderErrorApiResponse(eq(pluginApiRequestMock), errorCaptor.capture())).thenReturn(mock(GoPluginApiResponse.class)); diff --git a/src/test/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/SCMConfigurationRequestHandlerTests.java b/src/test/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/SCMConfigurationRequestHandlerTests.java index a4b87ff3..19b4a200 100644 --- a/src/test/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/SCMConfigurationRequestHandlerTests.java +++ b/src/test/java/com/thoughtworks/go/scm/plugin/model/requestHandlers/SCMConfigurationRequestHandlerTests.java @@ -19,7 +19,7 @@ public class SCMConfigurationRequestHandlerTests { @Mock private GoPluginApiRequest apiRequest; - private RequestHandler requestHandler = new SCMConfigurationRequestHandler(); + private final RequestHandler requestHandler = new SCMConfigurationRequestHandler(); @Test public void shouldReturnSuccessJsonResponseForScmConfigurationRequest() { diff --git a/src/test/java/com/thoughtworks/go/scm/plugin/util/JsonUtilsTests.java b/src/test/java/com/thoughtworks/go/scm/plugin/util/JsonUtilsTest.java similarity index 97% rename from src/test/java/com/thoughtworks/go/scm/plugin/util/JsonUtilsTests.java rename to src/test/java/com/thoughtworks/go/scm/plugin/util/JsonUtilsTest.java index e15f798b..77ea9ed4 100644 --- a/src/test/java/com/thoughtworks/go/scm/plugin/util/JsonUtilsTests.java +++ b/src/test/java/com/thoughtworks/go/scm/plugin/util/JsonUtilsTest.java @@ -2,8 +2,8 @@ import com.thoughtworks.go.plugin.api.request.GoPluginApiRequest; import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; +import com.thoughtworks.go.scm.plugin.git.GitConfig; import com.thoughtworks.go.scm.plugin.helpers.JsonHelper; -import com.tw.go.plugin.model.GitConfig; import org.junit.jupiter.api.Test; import java.io.IOException; @@ -15,10 +15,10 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class JsonUtilsTests { +public class JsonUtilsTest { - private class Response { - private String message; + private static class Response { + private final String message; Response(String message) { this.message = message; diff --git a/src/test/resources/sample-repository/branch-git-repository.zip b/src/test/resources/sample-repository/branch-git-repository.zip new file mode 100644 index 00000000..bd850db3 Binary files /dev/null and b/src/test/resources/sample-repository/branch-git-repository.zip differ diff --git a/src/test/resources/sample-repository/merge-commit-git-repository.zip b/src/test/resources/sample-repository/merge-commit-git-repository.zip new file mode 100644 index 00000000..e4270255 Binary files /dev/null and b/src/test/resources/sample-repository/merge-commit-git-repository.zip differ diff --git a/src/test/resources/sample-repository/simple-git-repository-1.zip b/src/test/resources/sample-repository/simple-git-repository-1.zip new file mode 100644 index 00000000..8574fcf5 Binary files /dev/null and b/src/test/resources/sample-repository/simple-git-repository-1.zip differ diff --git a/src/test/resources/sample-repository/simple-git-repository-2.zip b/src/test/resources/sample-repository/simple-git-repository-2.zip new file mode 100644 index 00000000..0184da96 Binary files /dev/null and b/src/test/resources/sample-repository/simple-git-repository-2.zip differ diff --git a/src/test/resources/sample-repository/simple-git-repository-3.zip b/src/test/resources/sample-repository/simple-git-repository-3.zip new file mode 100644 index 00000000..fc32f5a4 Binary files /dev/null and b/src/test/resources/sample-repository/simple-git-repository-3.zip differ diff --git a/src/test/resources/sample-repository/sub-module-git-repository.zip b/src/test/resources/sample-repository/sub-module-git-repository.zip new file mode 100644 index 00000000..a347de89 Binary files /dev/null and b/src/test/resources/sample-repository/sub-module-git-repository.zip differ