Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GitOps: do not mask exceptions, propagate instead #2943

Merged
merged 1 commit into from
Dec 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package org.scalafmt.sysops

import scala.sys.process.ProcessLogger
import scala.util.{Failure, Success, Try}
import java.nio.file.Path
import java.nio.file.{InvalidPathException, Path}

object GitOps {

Expand Down Expand Up @@ -39,83 +39,79 @@ trait GitOps {

private class GitOpsImpl(val workingDirectory: AbsoluteFile) extends GitOps {

private[scalafmt] def exec(cmd: Seq[String]): Try[Seq[String]] = {
private[scalafmt] def exec(cmd: Seq[String]): Seq[String] = {
execImpl(cmd).linesIterator.toSeq
}

private def execImpl(cmd: Seq[String]): String = {
val errors = Seq.newBuilder[String]
Try {
val swallowStderr = ProcessLogger(_ => (), errors += _)
sys.process.Process(cmd, workingDirectory.jfile).!!(swallowStderr)
}.fold(
e => {
} match {
case Failure(e) =>
val err = errors.result().mkString("\n> ", "\n> ", "\n")
val msg = s"Failed to run command ${cmd.mkString(" ")}. Error:$err"
Failure(new IllegalStateException(msg, e))
},
x => Success(x.trim.linesIterator.toSeq)
)
throw new IllegalStateException(msg, e)
case Success(x) => x.trim
}
}

override def lsTree(dir: AbsoluteFile*): Seq[AbsoluteFile] = {
val cmd = Seq("git", "ls-files", "--full-name") ++ dir.map(_.toString())
rootDir.fold(Seq.empty[AbsoluteFile]) { rtDir =>
exec(cmd)
.map(_.map(rtDir.join).filter(_.isRegularFile))
.getOrElse(Seq.empty)
}
withRoot(exec(cmd)).filter(_.isRegularFile)
}

override def rootDir: Option[AbsoluteFile] = {
override def rootDir: Option[AbsoluteFile] = tryRoot.toOption

private lazy val tryRoot: Try[AbsoluteFile] = {
val cmd = Seq("git", "rev-parse", "--show-toplevel")
for {
Seq(rootPath) <- exec(cmd).toOption
file <- AbsoluteFile.fromPathIfAbsolute(rootPath)
if file.isDirectory
} yield file
Try(execImpl(cmd)).flatMap { x =>
val file = AbsoluteFile(FileOps.getFile(x))
if (file.isDirectory) Success(file)
else Failure(new InvalidPathException(x, "not a directory"))
}
}

override def diff(branch: String, dir: AbsoluteFile*): Seq[AbsoluteFile] = {
val cmd = Seq("git", "diff", "--name-only", "--diff-filter=d", branch) ++
(if (dir.isEmpty) Seq.empty else "--" +: dir.map(_.toString()))
for {
root <- rootDir.toSeq
path <- exec(cmd).getOrElse(Seq.empty)
} yield root.join(path)
withRoot(exec(cmd))
}

override def status(dir: AbsoluteFile*): Seq[AbsoluteFile] = {
val cmd = Seq("git", "status", "--porcelain") ++
(if (dir.isEmpty) Seq.empty else "--" +: dir.map(_.toString()))
for {
root <- rootDir.toSeq
statusLine <- exec(cmd).getOrElse(Seq.empty)
file <- getFileFromGitStatusLine(statusLine).toOption.toSeq
} yield root.join(file)
}.filter(_.exists)
withRoot(exec(cmd).map(getFileFromGitStatusLine)).filter(_.exists)
}

private final val renameStatusCode = "R"
private final val renameStatusArrowDelimiter = "-> "

private def withRoot(files: => Seq[String]): Seq[AbsoluteFile] = {
val root = tryRoot.get
files.map(root.join)
}

/*
Method extracts path to changed file from the singular line of the `git status --porcelain` output.
(see https://git-scm.com/docs/git-status#_short_format)
*/
private def extractPathPart(s: String): Try[String] =
Try(
Option(s)
// Checks if the line status states the file was renamed (E.g: `R ORIG_PATH -> PATH`)
.filter(_.substring(0, 2).contains(renameStatusCode))
// takes the part of the string after the `-> ` character sequence
.map(_.split(renameStatusArrowDelimiter).last)
// fallback for the regular status line (E.g.: `XY PATH`)
// Drops the status codes by splitting on white spaces then taking the tail of the result
// Restores spaces in the path by merging the tail back with white space separator
.getOrElse(s.trim.split(' ').tail.mkString(" "))
.trim
)
private def extractPathPart(s: String): String =
Option(s)
// Checks if the line status states the file was renamed (E.g: `R ORIG_PATH -> PATH`)
.filter(_.substring(0, 2).contains(renameStatusCode))
// takes the part of the string after the `-> ` character sequence
.map(_.split(renameStatusArrowDelimiter).last)
// fallback for the regular status line (E.g.: `XY PATH`)
// Drops the status codes by splitting on white spaces then taking the tail of the result
// Restores spaces in the path by merging the tail back with white space separator
.getOrElse(s.trim.split(' ').tail.mkString(" "))
.trim

private def trimQuotes(s: String): String =
s.replaceAll("^\"|\"$", "")

private def getFileFromGitStatusLine(s: String): Try[Path] =
extractPathPart(s)
.map(pathRaw => FileOps.getFile(trimQuotes(pathRaw)))
private def getFileFromGitStatusLine(s: String): String =
trimQuotes(extractPathPart(s))
}
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ private object GitOpsTest {
ops: GitOpsImpl,
loc: Location
): Seq[String] =
ops.exec("git" +: cmd +: args) match {
Try(ops.exec("git" +: cmd +: args)) match {
case Failure(f) => Assertions.fail(s"Failed git command. Got: $f")
case Success(s) => s
}
Expand Down