All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.sbt.git.GitPlugin.scala Maven / Gradle / Ivy

package com.github.sbt.git

import sbt._
import Keys._

/** This plugin has all the basic 'git' functionality for other plugins. */
object SbtGit {

  object GitKeys {
    // Read-only git settings and values for use in other build settings.
    // Note: These are all grabbed using jgit currently.
    val gitReader = SettingKey[ReadableGit]("git-reader", "This gives us a read-only view of the git repository.")
    val gitBranch = SettingKey[Option[String]]("git-branch", "Target branch of a git operation")
    val gitCurrentBranch = SettingKey[String]("git-current-branch", "The current branch for this project.")
    val gitCurrentTags = SettingKey[Seq[String]]("git-current-tags", "The tags associated with this commit.")
    val gitHeadCommit = SettingKey[Option[String]]("git-head-commit", "The commit sha for the top commit of this project.")
    val gitHeadMessage = SettingKey[Option[String]]("git-head-message", "The message for the top commit of this project.")
    val gitHeadCommitDate = SettingKey[Option[String]]("git-head-commit-date", "The commit date for the top commit of this project in ISO-8601 format.")
    val gitDescribedVersion = SettingKey[Option[String]]("git-described-version", "Version as returned by `git describe --tags`.")
    val gitUncommittedChanges = SettingKey[Boolean]("git-uncommitted-changes", "Whether there are uncommitted changes.")

    // A Mechanism to run Git directly.
    val gitRunner = TaskKey[GitRunner]("git-runner", "The mechanism used to run git in the current build.")

    // Keys associated with setting a version number.
    val useGitDescribe = SettingKey[Boolean]("use-git-describe", "Get version by calling `git describe` on the repository")
    val gitDescribePatterns = SettingKey[Seq[String]]("git-describe-patterns", "Patterns to `--match` against when using `git describe`")
    val gitTagToVersionNumber = SettingKey[String => Option[String]]("git-tag-to-version-number", "Converts a git tag string to a version number.")

    // Component version strings.  We use these when determining the actual version.
    val formattedShaVersion = settingKey[Option[String]]("Completely formatted version string which will use the git SHA. Override this to change how the SHA version is formatted.")
    val formattedDateVersion = settingKey[String]("Completely formatted version string which does not rely on git.  Used as a fallback.")

    // Helper suffix/prefix information for generated default version strings.
    val baseVersion = SettingKey[String]("base-version", "The base version number which we will append the git version to.")
    val versionProperty = SettingKey[String]("version-property", "The system property that can be used to override the version number.  Defaults to `project.version`.")
    val uncommittedSignifier = SettingKey[Option[String]]("uncommitted-signifier", "Optional additional signifier to signify uncommitted changes")

    // The remote repository we're using.
    val gitRemoteRepo = SettingKey[String]("git-remote-repo", "The remote git repository associated with this project")

    // Git worktree workaround
    val useConsoleForROGit = SettingKey[Boolean]("console-ro-runner", "Whether to shell out to git for ro ops in the current build.")
  }

  object GitCommand {
    import complete._
    import complete.DefaultParsers._

    val action: (State, Seq[String]) => State = { (state, args) =>
      val extracted = Project.extract(state)
      val (state2, runner) = extracted.runTask(GitKeys.gitRunner, state)
      val dir = extracted.get(baseDirectory)
      runner(args:_*)(dir, state2.log)
      state2
    }

    // the git command we expose to the user
    val command: Command = Command("git")(s =>  fullCommand(s)){ (state, arg) =>
      val (command, args) = arg
      action(state, command +: args)
    }

    val QuotedString: Parser[String] = DQuoteClass ~> any.+.string.filter(!_.contains(DQuoteClass), _ => "Invalid quoted string") <~ DQuoteClass

    // the parser providing auto-completion for git command
    // Note: This isn't an exact parser for git, it just tries to make it more convenient in sbt with a modicum of autocomplete.
    // Ideally we'd use the bash autocompletion scripts or zsh ones for full and complete information, but this actually
    // gives us a lot of bang for the buck.
    def fullCommand(state: State) = {
      val extracted = Project.extract(state)
      val reader = extracted.get(GitKeys.gitReader)
      implicit val branches: Seq[String] = reader.withGit(_.branches) ++ reader.withGit(_.remoteBranches) :+ "HEAD"
      // let's not forget the user can define its own git commands and aliases so we don't want to parse the command
      // TODO we could though provide a list of available git commands
      // TODO some git commands like add take filepaths as arguments
      token(Space) ~> token(NotQuoted, "") ~ (Space ~> token(branch | QuotedString)).*
    }

    def branch(implicit branches: Seq[String]): Parser[String] = NotQuoted.examples(branches.toSet)

    private def isGitRepo(dir: File): Boolean = {
      if (System.getenv("GIT_DIR") != null) true
      else isGitDir(dir)
    }

    @scala.annotation.tailrec
    private def isGitDir(dir: File): Boolean = {
      if (dir.listFiles().map(_.getName).contains(".git")) true
      else {
        val parent = dir.getParentFile
        if (parent == null) false
        else isGitDir(parent)
      }
    }

    val prompt: State => String = { state =>
      val extracted = Project.extract(state)
      val reader = extracted get GitKeys.gitReader
      val dir = extracted get baseDirectory
      val name = extracted get Keys.name
      if (isGitRepo(dir)) {
        val branch = reader.withGit(_.branch)
        name + "(" + branch + ")> "
      } else {
        name + "> "
      }
    }
  }

  // Build settings.
  import GitKeys._
  def buildSettings = Seq(
    useConsoleForROGit := false,
    gitReader := new DefaultReadableGit(baseDirectory.value, if (useConsoleForROGit.value) Some(new ConsoleGitReadableOnly(ConsoleGitRunner, file("."), sLog.value)) else None),
    gitRunner := ConsoleGitRunner,
    gitHeadCommit := gitReader.value.withGit(_.headCommitSha),
    gitHeadMessage := gitReader.value.withGit(_.headCommitMessage),
    gitHeadCommitDate := gitReader.value.withGit(_.headCommitDate),
    gitTagToVersionNumber := git.defaultTagByVersionStrategy,
    gitDescribePatterns := Seq.empty[String],
    gitDescribedVersion := gitReader.value.withGit(_.describedVersion(git.gitDescribePatterns.value)).map(v => git.gitTagToVersionNumber.value(v).getOrElse(v)),
    gitCurrentTags := gitReader.value.withGit(_.currentTags),
    gitCurrentBranch := Option(gitReader.value.withGit(_.branch)).getOrElse(""),
    ThisBuild / gitUncommittedChanges := gitReader.value.withGit(_.hasUncommittedChanges),
    scmInfo := parseScmInfo(gitReader.value.withGit(_.remoteOrigin))
  )
  private[sbt] def parseScmInfo(remoteOrigin: String): Option[ScmInfo] = {
    val user = """(?:[^@\/]+@)?"""
    val domain = """([^\/]+)"""
    val gitPath = """(.*?)(?:\.git)?\/?$"""
    val unauthenticated = raw"""(?:git|https?|ftps?)\:\/\/$domain\/$gitPath""".r
    val ssh = raw"""ssh\:\/\/$user$domain\/$gitPath""".r
    val headlessSSH = raw"""$user$domain:$gitPath""".r

    def buildScmInfo(domain: String, repo: String): Option[ScmInfo] = Option(
      ScmInfo(
        url(s"https://$domain/$repo"),
        s"scm:git:https://$domain/$repo.git",
        Some(s"scm:git:git@$domain:$repo.git")
      )
    )

    remoteOrigin match {
      case unauthenticated(domain, repo) => buildScmInfo(domain,repo)
      case ssh(domain, repo) => buildScmInfo(domain,repo)
      case headlessSSH(domain, repo) => buildScmInfo(domain,repo)
      case _ => None
    }
  }

  val projectSettings = Seq(
    // Input task to run git commands directly.
    commands += GitCommand.command,
    gitTagToVersionNumber := git.defaultTagByVersionStrategy,
    gitDescribePatterns := Seq.empty[String],
    gitDescribedVersion := {
      val projectPatterns = gitDescribePatterns.value
      val buildPatterns = (ThisBuild / gitDescribePatterns).value
      val projectTagToVersionNumber = gitTagToVersionNumber.value
      val buildTagToVersionNumber = (ThisBuild / gitTagToVersionNumber).value
      if (projectPatterns == buildPatterns && projectTagToVersionNumber == buildTagToVersionNumber)
        (ThisBuild / gitDescribedVersion).value
      else gitReader.value.withGit(_.describedVersion(projectPatterns)).map(v => projectTagToVersionNumber(v).getOrElse(v))
    },
  )

  /** A Predefined setting to use JGit runner for git. */
  def useJGit: Setting[_] = ThisBuild / gitRunner := JGitRunner

  /** Setting to use console git for readable ops, to allow working with git worktrees */
  def useReadableConsoleGit: Setting[_] = ThisBuild / useConsoleForROGit := true

  /** Adapts the project prompt to show the current project name *and* the current git branch. */
  def showCurrentGitBranch: Setting[_] =
    shellPrompt := GitCommand.prompt


  /** Uses git to control versioning.
   *
   * Versioning runs through the following:
   *
   * 1. Looks at version-property settings, and checks the sys.props to see if this has a value.
   * 2. Looks at the project tags.  The first to match the `gitTagToVersionNumber` setting is used to assign the version.
   * 3. if we have a head commit, we attach this to the base version setting "."
   * 4. We append the current timestamp to the base version: "."
   */
  def versionWithGit: Seq[Setting[_]] =
    Seq(
      ThisBuild / versionProperty := "project.version",
      ThisBuild / uncommittedSignifier := Some("SNAPSHOT"),
      ThisBuild / useGitDescribe := false,
      ThisBuild / formattedShaVersion := {
        val base = git.baseVersion.?.value
        val suffix =
          git.makeUncommittedSignifierSuffix(git.gitUncommittedChanges.value, git.uncommittedSignifier.value)
        git.gitHeadCommit.value map { sha =>
          git.defaultFormatShaVersion(base, sha, suffix)
        }
      },
      ThisBuild / formattedDateVersion := {
        val base = git.baseVersion.?.value
        git.defaultFormatDateVersion(base, new java.util.Date)
      },
      ThisBuild / isSnapshot := {
        git.gitCurrentTags.value.isEmpty || git.gitUncommittedChanges.value
      },
      ThisBuild / version := {
        val overrideVersion =
          git.overrideVersion(git.versionProperty.value)
        val uncommittedSuffix =
          git.makeUncommittedSignifierSuffix(git.gitUncommittedChanges.value, git.uncommittedSignifier.value)
        val releaseVersion =
          git.releaseVersion(git.gitCurrentTags.value, (ThisBuild / gitTagToVersionNumber).value, uncommittedSuffix)
        val describedVersion =
          git.flaggedOptional(git.useGitDescribe.value, git.describeVersion((ThisBuild / gitDescribedVersion).value, uncommittedSuffix))
        val datedVersion = formattedDateVersion.value
        val commitVersion = formattedShaVersion.value
        //Now we fall through the potential version numbers...
        git.makeVersion(Seq(
          overrideVersion,
          releaseVersion,
          describedVersion,
          commitVersion
        )) getOrElse datedVersion // For when git isn't there at all.
      }
    )

  def versionProjectWithGit: Seq[Setting[_]] =
    Seq(
      ThisProject / useGitDescribe := false,
      ThisProject / version := {
        val overrideVersion =
          git.overrideVersion(git.versionProperty.value)
        val uncommittedSuffix =
          git.makeUncommittedSignifierSuffix(git.gitUncommittedChanges.value, git.uncommittedSignifier.value)
        val releaseVersion =
          git.releaseVersion(git.gitCurrentTags.value, (ThisProject / gitTagToVersionNumber).value, uncommittedSuffix)
        val describedVersion =
          git.flaggedOptional(git.useGitDescribe.value, git.describeVersion((ThisProject / gitDescribedVersion).value, uncommittedSuffix))
        val datedVersion = formattedDateVersion.value
        val commitVersion = formattedShaVersion.value
        //Now we fall through the potential version numbers...
        git.makeVersion(Seq(
          overrideVersion,
          releaseVersion,
          describedVersion,
          commitVersion
        )) getOrElse datedVersion // For when git isn't there at all.
      }
    )

  /** A holder of keys for simple config. */
  object git {
    val remoteRepo = GitKeys.gitRemoteRepo
    val branch = GitKeys.gitBranch
    val runner = ThisBuild / GitKeys.gitRunner
    val gitHeadCommit = ThisBuild / GitKeys.gitHeadCommit
    val gitHeadMessage = ThisBuild / GitKeys.gitHeadMessage
    val gitHeadCommitDate = ThisBuild / GitKeys.gitHeadCommitDate
    val useGitDescribe = ThisProject / GitKeys.useGitDescribe
    val gitDescribePatterns = ThisProject / GitKeys.gitDescribePatterns
    val gitDescribedVersion = ThisProject / GitKeys.gitDescribedVersion
    val gitCurrentTags = ThisBuild / GitKeys.gitCurrentTags
    val gitCurrentBranch = ThisBuild / GitKeys.gitCurrentBranch
    val gitTagToVersionNumber = ThisProject / GitKeys.gitTagToVersionNumber
    val baseVersion = ThisBuild / GitKeys.baseVersion
    val versionProperty = ThisBuild / GitKeys.versionProperty
    val gitUncommittedChanges = ThisBuild / GitKeys.gitUncommittedChanges
    val uncommittedSignifier = ThisBuild / GitKeys.uncommittedSignifier
    val formattedShaVersion = ThisBuild / GitKeys.formattedShaVersion
    val formattedDateVersion = ThisBuild / GitKeys.formattedDateVersion


    val defaultTagByVersionStrategy: String => Option[String] = { tag =>
      if(tag matches "v[0-9].*") Some(tag drop 1)
      else None
    }

    def defaultFormatShaVersion(baseVersion: Option[String], sha:String, suffix: String):String = {
      baseVersion.map(_ +"-").getOrElse("") + sha + suffix
    }

    def defaultFormatDateVersion(baseVersion:Option[String], date:java.util.Date):String = {
      val df = new java.text.SimpleDateFormat("yyyyMMdd'T'HHmmss")
      df setTimeZone java.util.TimeZone.getTimeZone("GMT")
      baseVersion.map(_ +"-").getOrElse("") + (df format (new java.util.Date))
    }

    def flaggedOptional(flag: Boolean, value: Option[String]): Option[String] =
      if(flag) value
      else None

    def makeUncommittedSignifierSuffix(hasUncommittedChanges: Boolean, uncommittedSignifier: Option[String]): String =
      flaggedOptional(hasUncommittedChanges, uncommittedSignifier).map("-" + _).getOrElse("")

    def describeVersion(gitDescribedVersion: Option[String], suffix: String): Option[String] = {
      gitDescribedVersion.map(_ + suffix)
    }

    def releaseVersion(currentTags: Seq[String], releaseTagVersion: String => Option[String], suffix: String): Option[String] = {
      val versions =
        for {
          tag <- currentTags
          version <- releaseTagVersion(tag)
        } yield version

      // NOTE - Selecting the last tag or the first tag should be an option.
      val highestVersion = versions.sortWith { versionsort.VersionHelper.compare(_, _) > 0 }.headOption
      highestVersion.map(_ + suffix)
    }

    def overrideVersion(versionProperty: String) = Option(sys.props(versionProperty))

    def makeVersion(versionPossibilities: Seq[Option[String]]): Option[String] = {
      versionPossibilities.reduce(_ orElse _)
    }
  }
}

/** The autoplugin which adapts the old sbt plugin classes into a legitimate AutoPlugin.
 *
 * This will add the ability to call git directly in the sbt shell via a command, as well as add
 * the infrastructure to read git properties.
 *
 * We keep the old SbtGit object around in an attempt not to break projects which depend on the old
 * plugin directly.
 */
object GitPlugin extends AutoPlugin {
  override def requires = sbt.plugins.CorePlugin
  override def trigger = allRequirements
  // Note: In an attempt to pretend we are binary compatible, we current add this as an after thought.
  // In 1.0, we should deprecate/move the other means of getting these values.
  object autoImport {
    val git = SbtGit.git
    def versionWithGit = SbtGit.versionWithGit
    def versionProjectWithGit = SbtGit.versionProjectWithGit
    def useJGit = SbtGit.useJGit
    def useReadableConsoleGit = SbtGit.useReadableConsoleGit
    def showCurrentGitBranch = SbtGit.showCurrentGitBranch
  }
  override def buildSettings: Seq[Setting[_]] = SbtGit.buildSettings
  override def projectSettings: Seq[Setting[_]] = SbtGit.projectSettings
}

/** Adapter to auto-enable git versioning.  i.e. the sbt 0.13.5+ mechanism of turning it on. */
object GitVersioning extends AutoPlugin {
  override def requires = sbt.plugins.IvyPlugin && GitPlugin
  override def buildSettings = GitPlugin.autoImport.versionWithGit
  override def projectSettings = GitPlugin.autoImport.versionProjectWithGit
}
/** Adapter to enable the git prompt. i.e. rich prompt based on git info. */
object GitBranchPrompt extends AutoPlugin {
  override def requires = GitPlugin
  override  def projectSettings = SbtGit.showCurrentGitBranch
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy