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