diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 88c0dd7c..b32d8063 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,7 +30,7 @@ jobs: - name: Upgrade platform run: sudo apt-get upgrade - name: Install git-svn - run: sudo apt-get install --yes git git-svn + run: sudo apt-get install --yes git git-svn expect - name: 🔍 Analyze code with SonarQube env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Dockerfile b/Dockerfile index fc6524e4..f110f030 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ ENV SPRING_OUTPUT_ANSI_ENABLED=ALWAYS \ JAVA_OPTS="" RUN apt update && \ - apt install -y git git-svn subversion + apt install -y git git-svn subversion expect COPY target/svn2git.jar /usr/svn2git/ diff --git a/pom.xml b/pom.xml index fc6095de..9a4d27c4 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ fr.yodamad.svn2git svn-2-git - 2.1.1 + 2.2.0 jar Svn 2 GitLab @@ -75,6 +75,7 @@ 3.5.0.1254 2.2.5 3.0.3 + 4.2.0 yodamad_svn2git @@ -425,6 +426,12 @@ ${kotlin.version} + + + com.github.jknack + handlebars + ${handlebars.version} + diff --git a/src/main/kotlin/fr/yodamad/svn2git/functions/CleaningFunctions.kt b/src/main/kotlin/fr/yodamad/svn2git/functions/CleaningFunctions.kt index 409ca6ac..0004dfe9 100644 --- a/src/main/kotlin/fr/yodamad/svn2git/functions/CleaningFunctions.kt +++ b/src/main/kotlin/fr/yodamad/svn2git/functions/CleaningFunctions.kt @@ -60,7 +60,7 @@ fun exceedsMaxSize(workUnit: WorkUnit, path: Path): Boolean { */ fun getListFromCommaSeparatedString(commaSeparatedStr: String?): List? { return if (StringUtils.isNotBlank(commaSeparatedStr)) { - commaSeparatedStr?.split("\\s*,\\s*")?.toTypedArray()?.toList() + commaSeparatedStr?.split(",") } else { ArrayList() } diff --git a/src/main/kotlin/fr/yodamad/svn2git/functions/GitFunctions.kt b/src/main/kotlin/fr/yodamad/svn2git/functions/GitFunctions.kt index 43cac7e5..4c780ab2 100644 --- a/src/main/kotlin/fr/yodamad/svn2git/functions/GitFunctions.kt +++ b/src/main/kotlin/fr/yodamad/svn2git/functions/GitFunctions.kt @@ -1,17 +1,16 @@ package fr.yodamad.svn2git.functions import fr.yodamad.svn2git.data.WorkUnit +import fr.yodamad.svn2git.io.Shell import fr.yodamad.svn2git.service.GitManager import fr.yodamad.svn2git.service.util.MASTER import fr.yodamad.svn2git.service.util.ORIGIN_TAGS -import fr.yodamad.svn2git.io.Shell import org.apache.commons.lang3.StringUtils import org.slf4j.LoggerFactory import java.io.BufferedReader import java.io.File import java.io.IOException import java.io.InputStreamReader -import java.util.* import java.util.stream.Collectors @@ -215,6 +214,10 @@ fun buildTrunk(workUnit: WorkUnit): String? { return if (mig.flat) { if (mig.svnGroup == mig.svnProject) { "--trunk=/" - } else String.format("--trunk=%s/", workUnit.migration.svnProject) - } else String.format("--trunk=%s/trunk", workUnit.migration.svnProject) + } else String.format("--trunk=%s/", workUnit.migration.svnProject.encode()) + } else String.format("--trunk=%s/trunk", workUnit.migration.svnProject.encode()) } + +fun buildSvnCompleteUrl(workUnit: WorkUnit) = + if (workUnit.migration.svnUrl.endsWith("/")) "${workUnit.migration.svnUrl}${workUnit.migration.svnGroup}" + else "${workUnit.migration.svnUrl}/${workUnit.migration.svnGroup}" diff --git a/src/main/kotlin/fr/yodamad/svn2git/functions/StringFunctions.kt b/src/main/kotlin/fr/yodamad/svn2git/functions/StringFunctions.kt index 4a49d4b9..e7859784 100644 --- a/src/main/kotlin/fr/yodamad/svn2git/functions/StringFunctions.kt +++ b/src/main/kotlin/fr/yodamad/svn2git/functions/StringFunctions.kt @@ -2,6 +2,8 @@ package fr.yodamad.svn2git.functions import fr.yodamad.svn2git.io.Shell.isWindows import org.apache.commons.lang3.StringUtils +import org.springframework.web.util.UriUtils.decode +import org.springframework.web.util.UriUtils.encode val EMPTY = "" @@ -11,3 +13,7 @@ fun formattedOrEmpty(element: String?, container: String, windowsCase: String? = windowsCase != null && isWindows -> String.format(container, element) else -> String.format(container, element) } + +fun String.encode(): String = encode(this, "UTF-8") +fun String.decode(): String = decode(this, "UTF-8") +fun String.gitFormat(): String = this.decode().replace(" ", "_") diff --git a/src/main/kotlin/fr/yodamad/svn2git/init/CheckUp.kt b/src/main/kotlin/fr/yodamad/svn2git/init/CheckUp.kt index aa9365c7..d51de968 100644 --- a/src/main/kotlin/fr/yodamad/svn2git/init/CheckUp.kt +++ b/src/main/kotlin/fr/yodamad/svn2git/init/CheckUp.kt @@ -22,6 +22,9 @@ class CheckUp { private val SVN_VERSION = "svn" private val SVN_ERROR = "⛔️ svn2git requires 'svn' v1+" + private val EXPECT_VERSION = "expect" + private val EXPECT_ERROR = "⛔️ expect binary is required on Linux. 👉 Run apt-get|yum install expect." + val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows") @PostConstruct @@ -29,6 +32,7 @@ class CheckUp { var allGood = checkGitSvnClone() allGood = allGood && checkCommand("git svn --version", GIT_SVN_VERSION, GIT_SVN_ERROR) allGood = allGood && checkCommand("svn --version", SVN_VERSION, SVN_ERROR) + if (!isWindows) allGood = allGood && checkCommand("expect -version", EXPECT_VERSION, EXPECT_ERROR) if (!allGood) exitProcess(1) } diff --git a/src/main/kotlin/fr/yodamad/svn2git/io/Shell.kt b/src/main/kotlin/fr/yodamad/svn2git/io/Shell.kt index 94a0abc4..b15842be 100644 --- a/src/main/kotlin/fr/yodamad/svn2git/io/Shell.kt +++ b/src/main/kotlin/fr/yodamad/svn2git/io/Shell.kt @@ -65,11 +65,12 @@ object Shell { */ @JvmOverloads @Throws(InterruptedException::class, IOException::class) - fun execCommand(commandManager: CommandManager, directory: String, command: String?, securedCommandToPrint: String? = command): Int { + fun execCommand(commandManager: CommandManager, directory: String, command: String?, securedCommandToPrint: String? = command, usePowershell: Boolean = false): Int { val builder = ProcessBuilder() val execDir = formatDirectory(directory) if (isWindows) { - builder.command("cmd.exe", "/c", command) + if (usePowershell) builder.command("powershell.exe", "-File", command) + else builder.command("cmd.exe", "/c", command) } else { builder.command("sh", "-c", command) } diff --git a/src/main/kotlin/fr/yodamad/svn2git/service/Cleaner.kt b/src/main/kotlin/fr/yodamad/svn2git/service/Cleaner.kt index 4edcac42..24781412 100644 --- a/src/main/kotlin/fr/yodamad/svn2git/service/Cleaner.kt +++ b/src/main/kotlin/fr/yodamad/svn2git/service/Cleaner.kt @@ -462,7 +462,7 @@ open class Cleaner(val historyMgr: HistoryManager, execCommand(workUnit.commandManager, workUnit.directory, svnBranchList) var elementsToKeep = Files.readAllLines(Paths.get(workUnit.directory, SVN_LIST)) .stream() - .map { l: String -> l.trim { it <= ' ' }.replace("/", "") } + .map { l: String -> l.trim { it <= ' ' }.replace("/", "").encode() } .collect(Collectors.toList()) // ######### Switch elementsToKeep if necessary ################################## diff --git a/src/main/kotlin/fr/yodamad/svn2git/service/GitManager.kt b/src/main/kotlin/fr/yodamad/svn2git/service/GitManager.kt index c51cedf7..0b7a96fb 100644 --- a/src/main/kotlin/fr/yodamad/svn2git/service/GitManager.kt +++ b/src/main/kotlin/fr/yodamad/svn2git/service/GitManager.kt @@ -8,6 +8,7 @@ import fr.yodamad.svn2git.domain.MigrationHistory import fr.yodamad.svn2git.domain.enumeration.StatusEnum import fr.yodamad.svn2git.domain.enumeration.StepEnum import fr.yodamad.svn2git.io.Shell.execCommand +import fr.yodamad.svn2git.io.Shell.isWindows import fr.yodamad.svn2git.repository.MappingRepository import fr.yodamad.svn2git.service.util.* import net.logstash.logback.encoder.org.apache.commons.lang.StringEscapeUtils @@ -79,36 +80,51 @@ open class GitManager(val historyMgr: HistoryManager, */ @Throws(IOException::class, InterruptedException::class) open fun gitSvnClone(workUnit: WorkUnit) { - val cloneCommand: String + var cloneCommand: String val safeCommand: String - if (!isEmpty(workUnit.migration.svnPassword)) { - val escapedPassword = StringEscapeUtils.escapeJava(workUnit.migration.svnPassword) - cloneCommand = gitCommandManager.initCommand(workUnit, workUnit.migration.svnUser, escapedPassword) - safeCommand = gitCommandManager.initCommand(workUnit, workUnit.migration.svnUser, STARS) - } else if (!isEmpty(applicationProperties.svn.password)) { - val escapedPassword = StringEscapeUtils.escapeJava(applicationProperties.svn.password) - cloneCommand = gitCommandManager.initCommand(workUnit, applicationProperties.svn.user, escapedPassword) - safeCommand = gitCommandManager.initCommand(workUnit, applicationProperties.svn.user, STARS) + if (!isWindows) { + if (!isEmpty(workUnit.migration.svnPassword)) { + val escapedPassword = StringEscapeUtils.escapeJava(workUnit.migration.svnPassword) + cloneCommand = gitCommandManager.initCommand(workUnit, workUnit.migration.svnUser, escapedPassword) + safeCommand = gitCommandManager.initCommand(workUnit, workUnit.migration.svnUser, STARS) + } else if (!isEmpty(applicationProperties.svn.password)) { + val escapedPassword = StringEscapeUtils.escapeJava(applicationProperties.svn.password) + cloneCommand = gitCommandManager.initCommand(workUnit, applicationProperties.svn.user, escapedPassword) + safeCommand = gitCommandManager.initCommand(workUnit, applicationProperties.svn.user, STARS) + } else { + cloneCommand = gitCommandManager.initCommand(workUnit, null, null) + safeCommand = cloneCommand + } + + // Waiting for Windows support... + cloneCommand = gitCommandManager.generateGitSvnCloneScript(workUnit, cloneCommand) } else { - cloneCommand = gitCommandManager.initCommand(workUnit, null, null) + val commandOptions = gitCommandManager.initOptions(workUnit) + gitCommandManager.generateGitSvnClonePackageForWindows(workUnit, commandOptions) + cloneCommand = "${workUnit.directory}\\git-command.ps1" safeCommand = cloneCommand } + val history = historyMgr.startStep(workUnit.migration, StepEnum.SVN_CHECKOUT, (if (workUnit.commandManager.isFirstAttemptMigration) "" else Constants.REEXECUTION_SKIPPING) + safeCommand) // Only Clone if first attempt at migration var cloneOK = true if (workUnit.commandManager.isFirstAttemptMigration) { try { - execCommand(workUnit.commandManager, workUnit.root, cloneCommand, safeCommand) + execCommand(workUnit.commandManager, workUnit.root, cloneCommand, safeCommand, true) } catch (thr: Throwable) { - LOG.warn("Cannot git svn clone", thr.message) cloneOK = false + LOG.warn("Cannot git svn clone", thr.message) var round = 0 var notOk = true while (round++ < applicationProperties.svn.maxFetchAttempts && notOk) { notOk = gitSvnFetch(workUnit, round) gitGC(workUnit, round) } + if (notOk) { + historyMgr.endStep(history, StatusEnum.FAILED, null) + throw RuntimeException() + } } } if (cloneOK) { @@ -131,7 +147,7 @@ open class GitManager(val historyMgr: HistoryManager, open fun gitSvnFetch(workUnit: WorkUnit, round: Int) : Boolean { val fetchCommand = "git svn fetch"; - val history = historyMgr.startStep(workUnit.migration, StepEnum.SVN_FETCH, "Round $round : $fetchCommand") + val history = historyMgr.startStep(workUnit.migration, StepEnum.SVN_FETCH, "$fetchCommand (Round $round)") return try { execCommand(workUnit.commandManager, workUnit.directory, fetchCommand) historyMgr.endStep(history, StatusEnum.DONE, null) diff --git a/src/main/kotlin/fr/yodamad/svn2git/service/util/GitBranchManager.kt b/src/main/kotlin/fr/yodamad/svn2git/service/util/GitBranchManager.kt index 8f491f1c..2a7914a5 100644 --- a/src/main/kotlin/fr/yodamad/svn2git/service/util/GitBranchManager.kt +++ b/src/main/kotlin/fr/yodamad/svn2git/service/util/GitBranchManager.kt @@ -3,6 +3,8 @@ package fr.yodamad.svn2git.service.util import fr.yodamad.svn2git.data.WorkUnit import fr.yodamad.svn2git.domain.enumeration.StatusEnum import fr.yodamad.svn2git.domain.enumeration.StepEnum +import fr.yodamad.svn2git.functions.decode +import fr.yodamad.svn2git.functions.gitFormat import fr.yodamad.svn2git.functions.listBranchesOnly import fr.yodamad.svn2git.io.Shell.execCommand import fr.yodamad.svn2git.service.GitManager @@ -30,12 +32,18 @@ open class GitBranchManager(val gitManager: GitManager, @Throws(RuntimeException::class) open fun pushBranch(workUnit: WorkUnit, branch: String): Boolean { var branchName = branch.replaceFirst("refs/remotes/origin/".toRegex(), "") - branchName = branchName.replaceFirst("origin/".toRegex(), "") + // Spaces aren't permitted, so replaced them with an underscore + branchName = branchName.replaceFirst("origin/".toRegex(), "").gitFormat() LOG.debug("Branch %s $branchName") val history = historyMgr.startStep(workUnit.migration, StepEnum.GIT_PUSH, branchName) + if (workUnit.migration.trunk != null && workUnit.migration.trunk != "trunk" && workUnit.migration.trunk.equals(branch.decode())) { + // Don't push branch that is used as new master + return true; + } + try { - execCommand(workUnit.commandManager, workUnit.directory, "git checkout -b $branchName $branch") + execCommand(workUnit.commandManager, workUnit.directory, "git checkout -b \"$branchName\" $branch") } catch (iEx: IOException) { LOG.error(FAILED_TO_PUSH_BRANCH, iEx) historyMgr.endStep(history, StatusEnum.FAILED, iEx.message) diff --git a/src/main/kotlin/fr/yodamad/svn2git/service/util/GitCommandManager.kt b/src/main/kotlin/fr/yodamad/svn2git/service/util/GitCommandManager.kt index b6f4e325..f76c482c 100644 --- a/src/main/kotlin/fr/yodamad/svn2git/service/util/GitCommandManager.kt +++ b/src/main/kotlin/fr/yodamad/svn2git/service/util/GitCommandManager.kt @@ -1,18 +1,24 @@ package fr.yodamad.svn2git.service.util +import com.github.jknack.handlebars.Handlebars import fr.yodamad.svn2git.config.ApplicationProperties import fr.yodamad.svn2git.data.WorkUnit import fr.yodamad.svn2git.domain.enumeration.StatusEnum import fr.yodamad.svn2git.domain.enumeration.StepEnum import fr.yodamad.svn2git.functions.* import fr.yodamad.svn2git.io.Shell +import fr.yodamad.svn2git.io.Shell.isWindows import fr.yodamad.svn2git.service.HistoryManager import fr.yodamad.svn2git.service.MappingManager import org.apache.commons.lang3.StringUtils.isEmpty import org.slf4j.LoggerFactory import org.springframework.stereotype.Service +import java.io.File import java.io.IOException +import java.io.StringWriter import java.net.URI +import java.nio.file.Files +import java.nio.file.attribute.PosixFilePermission.* @Service open class GitCommandManager(val historyMgr: HistoryManager, @@ -30,30 +36,68 @@ open class GitCommandManager(val historyMgr: HistoryManager, * @return */ open fun initCommand(workUnit: WorkUnit, username: String?, secret: String?): String { + val cloneCommand = String.format("git svn clone %s %s %s", + formattedOrEmpty(username, "--username %s"), + initOptions(workUnit), + buildSvnCompleteUrl(workUnit)) - // Get list of svnDirectoryDelete - val svnDirectoryDeleteList: List = mappingMgr.getSvnDirectoryDeleteList(workUnit.migration.id) - // Initialise ignorePaths string that will be passed to git svn clone - val ignorePaths: String = generateIgnorePaths(workUnit.migration.trunk, workUnit.migration.tags, workUnit.migration.branches, workUnit.migration.svnProject, svnDirectoryDeleteList) - - // regex with negative look forward allows us to choose the branch and tag names to keep - val ignoreRefs: String = generateIgnoreRefs(workUnit.migration.branchesToMigrate, workUnit.migration.tagsToMigrate) + // replace any multiple whitespaces and return + return cloneCommand.replace("\\s{2,}".toRegex(), " ").trim { it <= ' ' } + } - val cloneCommand = String.format("%s git svn clone %s %s %s %s %s %s %s %s %s%s", - formattedOrEmpty(secret, "echo %s |", "echo(%s|"), - formattedOrEmpty(username, "--username %s"), + open fun initOptions(workUnit: WorkUnit) : String { + val svnDirectoryDeleteList: List = mappingMgr.getSvnDirectoryDeleteList(workUnit.migration.id) + return String.format("%s %s %s %s %s %s", formattedOrEmpty(workUnit.migration.svnRevision, "-r%s:HEAD"), setTrunk(workUnit), setSvnElement("branches", workUnit.migration.branches, workUnit), setSvnElement("tags", workUnit.migration.tags, workUnit), - ignorePaths, ignoreRefs, + generateIgnorePaths(workUnit.migration.trunk, workUnit.migration.tags, workUnit.migration.branches, workUnit.migration.svnProject, svnDirectoryDeleteList), if (workUnit.migration.emptyDirs) "--preserve-empty-dirs" else if (workUnit.migration.emptyDirs == null && applicationProperties.getFlags().isGitSvnClonePreserveEmptyDirsOption) "--preserve-empty-dirs" else EMPTY, - if (workUnit.migration.svnUrl.endsWith("/")) workUnit.migration.svnUrl else "${workUnit.migration.svnUrl}/", - workUnit.migration.svnGroup) + ) + } + + open fun generateGitSvnCloneScript(workUnit: WorkUnit, gitSvnCloneCommand: String): String { + + val scriptInfo = ScriptInfo(gitSvnCloneCommand, workUnit.migration.svnUser, workUnit.migration.svnPassword, "${workUnit.directory}") + + val handlebars = Handlebars() + val template = handlebars.compile("templates/scripts/git-svn-clone.sh") + + val fileToWrite = File("${workUnit.directory}/git-svn-clone.sh") + val writer = StringWriter() + template.apply(scriptInfo, writer) + fileToWrite.writeText(writer.toString()) + + if (!isWindows) { + Files.setPosixFilePermissions( + fileToWrite.toPath(), + setOf(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE, GROUP_READ, OTHERS_READ) + ) + } + return fileToWrite.path + } + + open fun generateGitSvnClonePackageForWindows(workUnit: WorkUnit, cloneOptions: String?) { + + val scriptInfo = ScriptInfo("", workUnit.migration.svnUser, workUnit.migration.svnPassword, + "${workUnit.directory}", buildSvnCompleteUrl(workUnit), cloneOptions) + + val handlebars = Handlebars() + var template = handlebars.compile("templates/scripts/win/git-command.ps1") + + var fileToWrite = File("${workUnit.directory}/git-command.ps1") + var writer = StringWriter() + template.apply(scriptInfo, writer) + fileToWrite.writeText(writer.toString()) + + template = handlebars.compile("templates/scripts/win/git-svn-clone.ps1") + fileToWrite = File("${workUnit.directory}/git-svn-clone.ps1") + writer = StringWriter() + template.apply(null, writer) + fileToWrite.writeText(writer.toString()) - // replace any multiple whitespaces and return - return cloneCommand.replace("\\s{2,}".toRegex(), " ").trim { it <= ' ' } } /** @@ -140,3 +184,8 @@ open class GitCommandManager(val historyMgr: HistoryManager, else -> workUnit.migration.gitlabToken } } + +/** + * Info to inject in generated script + */ +data class ScriptInfo(val svnCommand: String, val svnUser: String, val svnPassword: String, val workingDir: String, val svnUrl: String? = "", val cloneOptions: String? = "") diff --git a/src/main/kotlin/fr/yodamad/svn2git/service/util/GitCommands.kt b/src/main/kotlin/fr/yodamad/svn2git/service/util/GitCommands.kt index d6722ee5..9067c80c 100644 --- a/src/main/kotlin/fr/yodamad/svn2git/service/util/GitCommands.kt +++ b/src/main/kotlin/fr/yodamad/svn2git/service/util/GitCommands.kt @@ -1,6 +1,8 @@ package fr.yodamad.svn2git.service.util import fr.yodamad.svn2git.data.WorkUnit +import fr.yodamad.svn2git.functions.encode +import fr.yodamad.svn2git.functions.gitFormat import fr.yodamad.svn2git.io.Shell.execCommand // Keywords @@ -21,8 +23,8 @@ fun deleteBranch(branch: String) = gitCommand(BRANCH, "-D", branch) fun renameBranch(branch: String) = gitCommand(BRANCH, "-m", branch) // Pull management -fun checkoutFromOrigin(branch: String) = gitCommand(CHECKOUT, "-b", "$branch refs/remotes/origin/$branch") -fun checkout(branch: String = MASTER) = gitCommand(CHECKOUT, target = branch) +fun checkoutFromOrigin(branch: String) = gitCommand(CHECKOUT, "-b", "${branch.gitFormat()} refs/remotes/origin/${branch.encode()}") +fun checkout(branch: String = MASTER) = gitCommand(CHECKOUT, target = branch.encode()) // Push management fun add(element: String) = gitCommand("add", target = element) @@ -32,7 +34,7 @@ fun push(branch: String = MASTER) = "$GIT_PUSH --set-upstream origin $branch" // Maintenance management fun gc() = gitCommand("gc") -fun resetHard(branch: String = MASTER) = gitCommand(RESET, "--hard", "origin/$branch") +fun resetHard(branch: String = MASTER) = gitCommand(RESET, "--hard", "origin/${branch.encode()}") fun resetHead() = gitCommand(RESET, "--hard", "HEAD") fun gitClean(commandManager: CommandManager, workUnit: WorkUnit) { try { diff --git a/src/main/kotlin/fr/yodamad/svn2git/service/util/GitTagManager.kt b/src/main/kotlin/fr/yodamad/svn2git/service/util/GitTagManager.kt index 0f7f6ed0..4ece02b8 100644 --- a/src/main/kotlin/fr/yodamad/svn2git/service/util/GitTagManager.kt +++ b/src/main/kotlin/fr/yodamad/svn2git/service/util/GitTagManager.kt @@ -11,7 +11,6 @@ import fr.yodamad.svn2git.service.HistoryManager import org.slf4j.LoggerFactory import org.springframework.stereotype.Service import java.io.IOException -import java.util.function.Consumer @Service open class GitTagManager(val gitManager: GitManager, @@ -28,10 +27,12 @@ open class GitTagManager(val gitManager: GitManager, * @param remotes */ open fun manageTags(workUnit: WorkUnit, remotes: List) { - listTagsOnly(remotes)?.forEach(Consumer { t: String -> + listTagsOnly(remotes)?.stream()?.filter { + t -> workUnit.migration.tagsToMigrate == null || workUnit.migration.tagsToMigrate.split(",").any { a -> t.endsWith(a) } + }?.forEach { t: String -> val warn: Boolean = pushTag(workUnit, t) gitCommandManager.sleepBeforePush(workUnit, warn) - }) + } } /** diff --git a/src/main/resources/templates/scripts/git-svn-clone.sh.hbs b/src/main/resources/templates/scripts/git-svn-clone.sh.hbs new file mode 100644 index 00000000..31a210e5 --- /dev/null +++ b/src/main/resources/templates/scripts/git-svn-clone.sh.hbs @@ -0,0 +1,55 @@ +#!/usr/bin/expect -f + +set cmd_git_clone "{{{ svnCommand }}}" +set timeout -1 + +# Procedure to execute git svn clone +proc gitSvnClone { user password } { + expect { + "(R)eject, accept (t)emporarily or accept (p)ermanently? " { + send -- "p\r" + exp_continue + } + + "Password for '$user': " { + send "$password\r" + exp_continue + } + + + -gl "couldn't truncate file*" { + puts "catching error... continue with git svn fetch" + return 1 + } + eof { return 0 } + } +} + +# Execute git svn clone command +eval spawn $cmd_git_clone +set git_clone_results [gitSvnClone "{{{ svnUser }}}" "{{{ svnPassword }}}"] + +# If successful git svn clone, ... +if { $git_clone_results == 0 } { + # Successful git clone + exit 0 +} + +# If git svn clone KO : git svn fetch, ... +if { $git_clone_results == 1 } { + #go in git reporsitory for fetching + cd {{{ workingDir }}} + set timeout -1 ;# no timeout + set tryrun 1 + while {$tryrun} { + spawn git svn fetch + set tryrun 0 + expect { + -gl "couldn't truncate file*" { + puts "catching error... continue fetching" + set tryrun 1 + exp_continue + } + } + } +} diff --git a/src/main/resources/templates/scripts/win/git-command.ps1.hbs b/src/main/resources/templates/scripts/win/git-command.ps1.hbs new file mode 100644 index 00000000..6f3d43ec --- /dev/null +++ b/src/main/resources/templates/scripts/win/git-command.ps1.hbs @@ -0,0 +1 @@ +{{{ workingDir }}}\\git-svn-clone.ps1 -repoUrl {{{ svnUrl }}} -username {{{ svnUser }}} -password {{{ svnPassword }}} -debugging -outputStdout -destination "{{{ workingDir }}}" -certificateAcceptResponse t -cloneOptions \"{{{ cloneOptions }}}\" diff --git a/src/main/resources/templates/scripts/win/git-svn-clone.ps1.hbs b/src/main/resources/templates/scripts/win/git-svn-clone.ps1.hbs new file mode 100644 index 00000000..72655475 --- /dev/null +++ b/src/main/resources/templates/scripts/win/git-svn-clone.ps1.hbs @@ -0,0 +1,153 @@ +[CmdletBinding()] +Param( + [Parameter(Mandatory)] + [string]$repoUrl, + [Parameter(Mandatory)] + [string]$username, + [Parameter(Mandatory)] + [string]$password, + [Parameter(Mandatory)] + [ValidateSet("r", "p", "t")] + [string]$certificateAcceptResponse, + [Parameter(Mandatory=$false)] + [AllowEmptyString()] + [string]$destination="", + [Parameter(Mandatory=$false)] + [string]$cloneOptions="", + [switch]$outputStdout, + [switch]$debugging +) + +# Shamelessly stolen from https://stackoverflow.com/a/54933303 +Function Await-Task { + param ( + [Parameter(ValueFromPipeline=$true, Mandatory=$true)] + $task + ) + + process { + while (-not $task.AsyncWaitHandle.WaitOne(1000)) { if($debugging.IsPresent) { Write-Host "-> Waiting for task to complete" } } + $task.GetAwaiter().GetResult() + } +} + +Function Read-Output { + param ( + [Parameter(ValueFromPipeline=$true, Mandatory=$true)] + $streamReader + ) + + process { + $readContent = "" + $bufferSize = 80 + $buffer = [Char[]]::new($bufferSize) + do { + if($debugging.IsPresent) { + Write-Host "Task: reading output" + } + $readCount = $streamReader.ReadAsync($buffer, 0, $bufferSize) | Await-Task + $readContent += $buffer[0..$readCount] -join '' + } While($se.Peek() -ne -1) + + return $readContent + } +} + +$gitProgramPath = where.exe git +$gitArguments= "svn clone $repoUrl $destination --username=$username $cloneOptions" + +$p = New-Object System.Diagnostics.Process; +$p.StartInfo.UseShellExecute = $false; +$p.StartInfo.FileName = $gitProgramPath; +$p.StartInfo.Arguments = $gitArguments +$p.StartInfo.CreateNoWindow = $true +$p.StartInfo.RedirectStandardInput = $true +$p.StartInfo.RedirectStandardOutput = $true +$p.StartInfo.RedirectStandardError = $true + +if($debugging.IsPresent) { + Write-Host "Starting command : $gitProgramPath $gitArguments" +} + +[void]$p.Start() + +$sw = $p.StandardInput +$sr = $p.StandardOutput +$se = $p.StandardError + +if($debugging.IsPresent) { + Write-Host "Waiting 5 seconds" +} + +Start-Sleep -Seconds 5 + +if($debugging.IsPresent) { + Write-Host "Reading Standard Error" +} + +$readText = $se | Read-Output + +if($debugging.IsPresent) { + Write-Host "Text from Standard Error:`n$readText" +} + +if($debugging.IsPresent) { + Write-Host "Try finding predicate `"Couldn't chdir to `"" +} + +if($readText.Contains("Couldn't chdir to ")) { + Write-Error "git svn is still running, please kill perl.exe and relaunch the command" + exit 1 +} +if($debugging.IsPresent) { + Write-Host "Try finding predicate `"(R)eject, accept (t)emporarily or accept (p)ermanently?`"" +} +if($readText.Contains("(R)eject, accept (t)emporarily or accept (p)ermanently?")) { + if($debugging.IsPresent) { + Write-Host "Found predicate `"(R)eject, accept (t)emporarily or accept (p)ermanently?`"" + Write-Host "Sending response : $certificateAcceptResponse" + } + + switch($certificateAcceptResponse) { + "r" { Write-Host "Rejecting certificate" } + "t" { Write-Host "Accepting certificate temporarily" } + "p" { Write-Host "Accepting certificate permanently" } + } + $sw.WriteLine($certificateAcceptResponse) + if($debugging.IsPresent) { + Write-Host "Waiting 5 seconds" + } + + Start-Sleep -Seconds 5 + + if($debugging.IsPresent) { + Write-Host "Reading Standard Error" + } + $readText = $se | Read-Output +} + +if($debugging.IsPresent) { + Write-Host "Try finding predicate `"Password for `"" +} + +if($readText.Contains("Password for ")) { + if($debugging.IsPresent) { + Write-Host "Found predicate `"Password for `"" + } + Write-Host "Entering password" + $sw.WriteLine($password) +} + +if($debugging.IsPresent) { + Write-Host "Waiting for git svn command to complete ..." +} + +$p.WaitForExit(); + +if($debugging.IsPresent) { + Write-Host "Exited" +} + +if($outputStdout.IsPresent) { + Write-Host ($p.StandardOutput.ReadToEnd()) +} diff --git a/src/test/java/fr/yodamad/svn2git/data/Repository.java b/src/test/java/fr/yodamad/svn2git/data/Repository.java index 20bbdfa1..ac0ec51a 100644 --- a/src/test/java/fr/yodamad/svn2git/data/Repository.java +++ b/src/test/java/fr/yodamad/svn2git/data/Repository.java @@ -49,12 +49,21 @@ public static Repository flat() { return repository; } + public static Repository weird() { + Repository repository = new Repository(); + repository.name = "weird"; + repository.namespace = "weird"; + repository.keep.add(Files.REVISION); + return repository; + } + public class Files { public static final String REVISION = "revision.txt"; public static final String FILE_BIN = "file.bin"; public static final String DEEP_FILE = "deep.file"; public static final String FLAT_FILE = "flat.file"; - public static final String ANOTHER_BIN = Dirs.FOLDER + "another.bin"; + public static final String ROOT_ANOTHER_BIN = "another.bin"; + public static final String ANOTHER_BIN = Dirs.FOLDER + ROOT_ANOTHER_BIN; public static final String MAPPED_ANOTHER_BIN = Dirs.DIRECTORY + "another.bin"; public static final String JAVA = Dirs.FOLDER + "App.java"; public static final String MAPPED_JAVA = Dirs.DIRECTORY + "App.java"; diff --git a/src/test/java/fr/yodamad/svn2git/e2e/ComplexRepoTests.java b/src/test/java/fr/yodamad/svn2git/e2e/ComplexRepoTests.java index 30b7ef21..c7ad1915 100644 --- a/src/test/java/fr/yodamad/svn2git/e2e/ComplexRepoTests.java +++ b/src/test/java/fr/yodamad/svn2git/e2e/ComplexRepoTests.java @@ -12,6 +12,7 @@ import org.gitlab4j.api.models.Branch; import org.gitlab4j.api.models.Project; import org.gitlab4j.api.models.Tag; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -58,6 +59,15 @@ public void cleanGitlab() throws GitLabApiException { if (project.isPresent()) api.getProjectApi().deleteProject(project.get().getId()); } + @After + public void forceCleanGitlab() throws GitLabApiException, InterruptedException { + Optional project = api.getProjectApi().getOptionalProject(complex().namespace, complex().name); + if (project.isPresent()) api.getProjectApi().deleteProject(project.get().getId()); + while(api.getProjectApi().getOptionalProject(complex().namespace, complex().name).isPresent()) { + Thread.sleep(500); + } + } + @Test public void test_migration_on_complex_repository() throws ExecutionException, InterruptedException, GitLabApiException { Migration migration = initComplexMigration(applicationProperties); @@ -80,7 +90,61 @@ public void test_migration_on_complex_repository() throws ExecutionException, In branches.stream().filter(b -> b.getName().equals("master")).forEach(b -> hasHistory(project, b.getName())); // Check tags - List tags = checkTags(project); + List tags = checkTags(project, 5); + tags.forEach(t -> hasNoHistory(project, t.getName())); + } + + @Test + public void test_migration_with_filter_tags() throws ExecutionException, InterruptedException, GitLabApiException { + Migration migration = initComplexMigration(applicationProperties); + migration.setSvnHistory("all"); + migration.setTrunk("trunk"); + migration.setTags("*"); + migration.setTagsToMigrate("v1.0,v1.1"); + migration.setBranches("*"); + + startAndCheck(migration); + + // Check project + Optional project = checkProject(); + + // Check files + checkAllFiles(project); + + // Check branches + List branches = checkBranches(project); + branches.stream().filter(b -> !b.getName().equals("master")).forEach(b -> hasNoHistory(project, b.getName())); + branches.stream().filter(b -> b.getName().equals("master")).forEach(b -> hasHistory(project, b.getName())); + + // Check tags + List tags = checkTags(project, 2); + tags.forEach(t -> hasNoHistory(project, t.getName())); + } + + @Test + public void test_migration_with_filter_branches() throws ExecutionException, InterruptedException, GitLabApiException { + Migration migration = initComplexMigration(applicationProperties); + migration.setSvnHistory("all"); + migration.setTrunk("trunk"); + migration.setTags("*"); + migration.setBranchesToMigrate("v1.0"); + migration.setBranches("*"); + + startAndCheck(migration); + + // Check project + Optional project = checkProject(); + + // Check files + checkAllFiles(project); + + // Check branches + List branches = checkBranches(project, 1); + branches.stream().filter(b -> !b.getName().equals("master")).forEach(b -> hasNoHistory(project, b.getName())); + branches.stream().filter(b -> b.getName().equals("master")).forEach(b -> hasHistory(project, b.getName())); + + // Check tags + List tags = checkTags(project, 5); tags.forEach(t -> hasNoHistory(project, t.getName())); } diff --git a/src/test/java/fr/yodamad/svn2git/e2e/WeirdRepoTests.java b/src/test/java/fr/yodamad/svn2git/e2e/WeirdRepoTests.java new file mode 100644 index 00000000..6a29a358 --- /dev/null +++ b/src/test/java/fr/yodamad/svn2git/e2e/WeirdRepoTests.java @@ -0,0 +1,132 @@ +package fr.yodamad.svn2git.e2e; + +import fr.yodamad.svn2git.Svn2GitApp; +import fr.yodamad.svn2git.config.ApplicationProperties; +import fr.yodamad.svn2git.domain.Migration; +import fr.yodamad.svn2git.domain.enumeration.StatusEnum; +import fr.yodamad.svn2git.repository.MigrationRepository; +import fr.yodamad.svn2git.service.MigrationManager; +import fr.yodamad.svn2git.utils.Checks; +import org.gitlab4j.api.GitLabApi; +import org.gitlab4j.api.GitLabApiException; +import org.gitlab4j.api.models.Branch; +import org.gitlab4j.api.models.Project; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import javax.annotation.PostConstruct; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import static fr.yodamad.svn2git.data.Repository.Files.*; +import static fr.yodamad.svn2git.data.Repository.weird; +import static fr.yodamad.svn2git.utils.Checks.*; +import static fr.yodamad.svn2git.utils.MigrationUtils.initWeirdMigration; +import static java.lang.Thread.sleep; +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Svn2GitApp.class) +public class WeirdRepoTests { + + @Autowired + private MigrationManager migrationManager; + @Autowired + private MigrationRepository migrationRepository; + @Autowired + private ApplicationProperties applicationProperties; + private GitLabApi api; + + @PostConstruct + public void initApi() { + api = new GitLabApi(applicationProperties.gitlab.url, applicationProperties.gitlab.token); + Checks.initApi(applicationProperties); + } + + @Before + public void cleanGitlab() throws GitLabApiException { + Optional project = api.getProjectApi().getOptionalProject(weird().namespace, weird().name); + if (project.isPresent()) api.getProjectApi().deleteProject(project.get().getId()); + } + + @After + public void forceCleanGitlab() throws GitLabApiException, InterruptedException { + Optional project = api.getProjectApi().getOptionalProject(weird().namespace, weird().name); + if (project.isPresent()) api.getProjectApi().deleteProject(project.get().getId()); + while(api.getProjectApi().getOptionalProject(weird().namespace, weird().name).isPresent()) { + sleep(500); + } + } + + @Test + public void test_migration_with_space_in_trunk_name() throws ExecutionException, InterruptedException, GitLabApiException { + Migration migration = initWeirdMigration(applicationProperties); + migration.setSvnHistory("all"); + migration.setTrunk("branch with space"); + migration.setTags(null); + migration.setBranches("*"); + + startAndCheck(migration); + + // Check project + Optional project = checkProject(); + + // Check files + isPresent(project.get(), ROOT_ANOTHER_BIN, false); + isPresent(project.get(), FILE_BIN, false); + isPresent(project.get(), REVISION, false); + + // Check branches + List branches = checkBranches(project, 3); + + // Check tags + checkTags(project, 0); + } + + @Test + public void test_migration_with_space_in_branch_name() throws ExecutionException, InterruptedException, GitLabApiException { + Migration migration = initWeirdMigration(applicationProperties); + migration.setSvnHistory("all"); + migration.setTrunk("trunk"); + migration.setTags(null); + migration.setBranches("*"); + + startAndCheck(migration); + + // Check project + Optional project = checkProject(); + + // Check files + isMissing(project.get(), ROOT_ANOTHER_BIN); + isPresent(project.get(), FILE_BIN, false); + isPresent(project.get(), REVISION, false); + + // Check branches + List branches = checkBranches(project, 3); + assertThat(branches.stream().anyMatch(b -> b.getName().equals("branch_with_space"))).isTrue(); + + // Check tags + checkTags(project, 0); + } + + private void startAndCheck(Migration migration) throws ExecutionException, InterruptedException { + Migration saved = migrationRepository.save(migration); + Future result = migrationManager.startMigration(saved.getId(), false); + // Wait for async + result.get(); + + Migration closed = migrationRepository.findById(saved.getId()).get(); + assertThat(closed.getStatus()).isEqualTo(StatusEnum.DONE); + } + + private static Optional checkProject() { + return Checks.checkProject(weird()); + } +} diff --git a/src/test/java/fr/yodamad/svn2git/utils/MigrationUtils.java b/src/test/java/fr/yodamad/svn2git/utils/MigrationUtils.java index f6dc9624..e3a7e427 100644 --- a/src/test/java/fr/yodamad/svn2git/utils/MigrationUtils.java +++ b/src/test/java/fr/yodamad/svn2git/utils/MigrationUtils.java @@ -19,6 +19,11 @@ public static Migration initFlatMigration(ApplicationProperties props) { return mig; } + public static Migration initWeirdMigration(ApplicationProperties props) { + Migration mig = initMigration(weird(), props); + return mig; + } + public static Migration initComplexMigration(ApplicationProperties props) { Migration mig = initMigration(complex(), props); String name = format("/%s", complex().name); diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml index f9c17c91..982fc633 100644 --- a/src/test/resources/config/application.yml +++ b/src/test/resources/config/application.yml @@ -159,6 +159,7 @@ application: password: ENC(NCCvax0umvRUhKRJv91NYNzNao04KDprAgsLRWOBIQOCh1zDfZ6BekY3kBfQag87) credentials: required svnUrlModifiable: true + maxFetchAttempts: 1 override: extensions: false mappings: false