add full support for commit references - fixes #276

Sat, 22 Jul 2023 22:32:04 +0200

author
Mike Becker <universe@uap-core.de>
date
Sat, 22 Jul 2023 22:32:04 +0200
changeset 284
671c1c8fbf1c
parent 283
ea6181255423
child 285
8da71efbaa35

add full support for commit references - fixes #276

setup/postgres/psql_create_tables.sql file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/entities/Project.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/types/VcsType.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt file | annotate | diff | comparison | revisions
src/main/resources/localization/strings.properties file | annotate | diff | comparison | revisions
src/main/resources/localization/strings_de.properties file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/changelogs/changelog-de.jspf file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/changelogs/changelog.jspf file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/issue-view.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/project-form.jsp file | annotate | diff | comparison | revisions
     1.1 --- a/setup/postgres/psql_create_tables.sql	Sat Jul 22 15:07:23 2023 +0200
     1.2 +++ b/setup/postgres/psql_create_tables.sql	Sat Jul 22 22:32:04 2023 +0200
     1.3 @@ -1,6 +1,3 @@
     1.4 --- This script creates the module management tables
     1.5 ---
     1.6 -
     1.7  create table lpit_user
     1.8  (
     1.9      userid    serial primary key,
    1.10 @@ -10,6 +7,8 @@
    1.11      givenname text
    1.12  );
    1.13  
    1.14 +create type vcstype as enum ('None', 'Mercurial', 'Git');
    1.15 +
    1.16  create table lpit_project
    1.17  (
    1.18      projectid   serial primary key,
    1.19 @@ -18,6 +17,7 @@
    1.20      ordinal     integer not null default 0,
    1.21      description text,
    1.22      repoUrl     text,
    1.23 +    vcs         vcstype not null default 'None'::vcstype,
    1.24      owner       integer references lpit_user (userid)
    1.25  );
    1.26  
    1.27 @@ -168,3 +168,12 @@
    1.28  );
    1.29  
    1.30  create unique index lpit_issue_relation_unique on lpit_issue_relation (from_issue, to_issue, type);
    1.31 +
    1.32 +create table lpit_commit_ref
    1.33 +(
    1.34 +    issueid      integer not null references lpit_issue (issueid) on delete cascade,
    1.35 +    commit_hash  text    not null,
    1.36 +    commit_brief text    not null
    1.37 +);
    1.38 +
    1.39 +create unique index lpit_commit_ref_unique on lpit_commit_ref (issueid, commit_hash);
     2.1 --- a/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt	Sat Jul 22 15:07:23 2023 +0200
     2.2 +++ b/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt	Sat Jul 22 22:32:04 2023 +0200
     2.3 @@ -173,6 +173,10 @@
     2.4          }
     2.5      }
     2.6  
     2.7 +    val body: String by lazy {
     2.8 +        request.reader.lineSequence().joinToString("\n")
     2.9 +    }
    2.10 +
    2.11      private fun forward(jsp: String) {
    2.12          request.getRequestDispatcher(jspPath(jsp)).forward(request, response)
    2.13      }
     3.1 --- a/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt	Sat Jul 22 15:07:23 2023 +0200
     3.2 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt	Sat Jul 22 22:32:04 2023 +0200
     3.3 @@ -26,6 +26,7 @@
     3.4  package de.uapcore.lightpit.dao
     3.5  
     3.6  import de.uapcore.lightpit.entities.*
     3.7 +import de.uapcore.lightpit.types.CommitRef
     3.8  import de.uapcore.lightpit.viewmodel.ComponentSummary
     3.9  import de.uapcore.lightpit.viewmodel.IssueSummary
    3.10  import de.uapcore.lightpit.viewmodel.VersionSummary
    3.11 @@ -70,6 +71,8 @@
    3.12      fun collectIssueSummary(project: Project): IssueSummary
    3.13      fun collectIssueSummary(assignee: User): IssueSummary
    3.14  
    3.15 +    fun mergeCommitRefs(refs: List<CommitRef>)
    3.16 +
    3.17      fun listIssues(project: Project, includeDone: Boolean): List<Issue>
    3.18      fun listIssues(project: Project, includeDone: Boolean, version: Version?, component: Component?): List<Issue>
    3.19      fun findIssue(id: Int): Issue?
    3.20 @@ -101,4 +104,5 @@
    3.21       * Lists the issue comment history of the project with [projectId] for the past [days].
    3.22       */
    3.23      fun listIssueCommentHistory(projectId: Int, days: Int): List<IssueCommentHistoryEntry>
    3.24 +    fun listCommitRefs(issue: Issue): List<CommitRef>
    3.25  }
     4.1 --- a/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt	Sat Jul 22 15:07:23 2023 +0200
     4.2 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt	Sat Jul 22 22:32:04 2023 +0200
     4.3 @@ -26,6 +26,7 @@
     4.4  package de.uapcore.lightpit.dao
     4.5  
     4.6  import de.uapcore.lightpit.entities.*
     4.7 +import de.uapcore.lightpit.types.CommitRef
     4.8  import de.uapcore.lightpit.types.IssueHistoryType
     4.9  import de.uapcore.lightpit.types.RelationType
    4.10  import de.uapcore.lightpit.types.WebColor
    4.11 @@ -358,7 +359,7 @@
    4.12      //language=SQL
    4.13      private val projectQuery =
    4.14          """
    4.15 -        select projectid, name, node, ordinal, description, repourl,
    4.16 +        select projectid, name, node, ordinal, description, vcs, repourl,
    4.17              userid, username, lastname, givenname, mail
    4.18          from lpit_project
    4.19          left join lpit_user owner on lpit_project.owner = owner.userid
    4.20 @@ -370,6 +371,7 @@
    4.21              node = getString("node")
    4.22              ordinal = getInt("ordinal")
    4.23              description = getString("description")
    4.24 +            vcs = getEnum("vcs")
    4.25              repoUrl = getString("repourl")
    4.26              owner = extractOptionalUser()
    4.27          }
    4.28 @@ -381,6 +383,7 @@
    4.29              setStringSafe(i++, node)
    4.30              setInt(i++, ordinal)
    4.31              setStringOrNull(i++, description)
    4.32 +            setEnum(i++, vcs)
    4.33              setStringOrNull(i++, repoUrl)
    4.34              setIntOrNull(i++, owner?.id)
    4.35          }
    4.36 @@ -405,14 +408,14 @@
    4.37          }
    4.38  
    4.39      override fun insertProject(project: Project) {
    4.40 -        withStatement("insert into lpit_project (name, node, ordinal, description, repourl, owner) values (?, ?, ?, ?, ?, ?)") {
    4.41 +        withStatement("insert into lpit_project (name, node, ordinal, description, vcs, repourl, owner) values (?, ?, ?, ?, ?::vcstype, ?, ?)") {
    4.42              setProject(1, project)
    4.43              executeUpdate()
    4.44          }
    4.45      }
    4.46  
    4.47      override fun updateProject(project: Project) {
    4.48 -        withStatement("update lpit_project set name = ?, node = ?, ordinal = ?, description = ?, repourl = ?, owner = ? where projectid = ?") {
    4.49 +        withStatement("update lpit_project set name = ?, node = ?, ordinal = ?, description = ?, vcs = ?::vcstype, repourl = ?, owner = ? where projectid = ?") {
    4.50              val col = setProject(1, project)
    4.51              setInt(col, project.id)
    4.52              executeUpdate()
    4.53 @@ -471,6 +474,17 @@
    4.54              }
    4.55          }
    4.56  
    4.57 +    override fun mergeCommitRefs(refs: List<CommitRef>) {
    4.58 +        withStatement("insert into lpit_commit_ref (issueid, commit_hash, commit_brief) values (?,?,?) on conflict do nothing") {
    4.59 +            refs.forEach { ref ->
    4.60 +                setInt(1, ref.issueId)
    4.61 +                setString(2, ref.hash)
    4.62 +                setString(3, ref.message)
    4.63 +                executeUpdate()
    4.64 +            }
    4.65 +        }
    4.66 +    }
    4.67 +
    4.68      //</editor-fold>
    4.69  
    4.70      //<editor-fold desc="Issue">
    4.71 @@ -636,6 +650,18 @@
    4.72          }
    4.73      }
    4.74  
    4.75 +    override fun listCommitRefs(issue: Issue): List<CommitRef> =
    4.76 +        withStatement("select commit_hash, commit_brief from lpit_commit_ref where issueid = ?") {
    4.77 +            setInt(1, issue.id)
    4.78 +            queryAll {
    4.79 +                CommitRef(
    4.80 +                    issueId = issue.id,
    4.81 +                    hash = it.getString("commit_hash"),
    4.82 +                    message = it.getString("commit_brief")
    4.83 +                )
    4.84 +            }
    4.85 +        }
    4.86 +
    4.87      //</editor-fold>
    4.88  
    4.89      //<editor-fold desc="Issue Relations">
     5.1 --- a/src/main/kotlin/de/uapcore/lightpit/entities/Project.kt	Sat Jul 22 15:07:23 2023 +0200
     5.2 +++ b/src/main/kotlin/de/uapcore/lightpit/entities/Project.kt	Sat Jul 22 22:32:04 2023 +0200
     5.3 @@ -25,11 +25,14 @@
     5.4  
     5.5  package de.uapcore.lightpit.entities
     5.6  
     5.7 +import de.uapcore.lightpit.types.VcsType
     5.8 +
     5.9  data class Project(override val id: Int) : Entity, HasNode {
    5.10      var name: String = ""
    5.11      override var node: String = name
    5.12      var ordinal = 0
    5.13      var description: String? = null
    5.14 +    var vcs: VcsType = VcsType.None
    5.15      var repoUrl: String? = null
    5.16      var owner: User? = null
    5.17  }
    5.18 \ No newline at end of file
     6.1 --- a/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt	Sat Jul 22 15:07:23 2023 +0200
     6.2 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt	Sat Jul 22 22:32:04 2023 +0200
     6.3 @@ -46,6 +46,7 @@
     6.4          get("/%project/edit", this::projectForm)
     6.5          get("/-/create", this::projectForm)
     6.6          post("/-/commit", this::projectCommit)
     6.7 +        post("/%project/vcs/analyze", this::vcsAnalyze)
     6.8  
     6.9          get("/%project/versions/", this::versions)
    6.10          get("/%project/versions/%version/edit", this::versionForm)
    6.11 @@ -243,6 +244,7 @@
    6.12              description = http.param("description") ?: ""
    6.13              ordinal = http.param("ordinal")?.toIntOrNull() ?: 0
    6.14              repoUrl = http.param("repoUrl") ?: ""
    6.15 +            vcs = VcsType.valueOf(http.param("vcs") ?: "None")
    6.16              owner = (http.param("owner")?.toIntOrNull() ?: -1).let {
    6.17                  if (it < 0) null else dao.findUser(it)
    6.18              }
    6.19 @@ -261,6 +263,26 @@
    6.20          http.renderCommit("projects/${project.node}")
    6.21      }
    6.22  
    6.23 +    private fun vcsAnalyze(http: HttpRequest, dao: DataAccessObject) {
    6.24 +        val projectInfo = obtainProjectInfo(http, dao)
    6.25 +        if (projectInfo == null) {
    6.26 +            http.response.sendError(404)
    6.27 +            return
    6.28 +        }
    6.29 +
    6.30 +        // if analysis is not configured, reject the request
    6.31 +        if (projectInfo.project.vcs == VcsType.None) {
    6.32 +            http.response.sendError(404)
    6.33 +            return
    6.34 +        }
    6.35 +
    6.36 +        // obtain the list of issues for this project to filter cross-project references
    6.37 +        val knownIds = dao.listIssues(projectInfo.project, true).map { it.id }
    6.38 +
    6.39 +        // read the provided commit log and merge only the refs that relate issues from the current project
    6.40 +        dao.mergeCommitRefs(parseCommitRefs(http.body).filter { knownIds.contains(it.issueId) })
    6.41 +    }
    6.42 +
    6.43      private fun versions(http: HttpRequest, dao: DataAccessObject) {
    6.44          val projectInfo = obtainProjectInfo(http, dao)
    6.45          if (projectInfo == null) {
    6.46 @@ -475,7 +497,8 @@
    6.47                      component,
    6.48                      dao.listIssues(project, true),
    6.49                      dao.listIssueRelations(issue),
    6.50 -                    relationError
    6.51 +                    relationError,
    6.52 +                    dao.listCommitRefs(issue)
    6.53                  )
    6.54                  feedPath = feedPath(projectInfo.project)
    6.55                  navigationMenu = activeProjectNavMenu(
     7.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     7.2 +++ b/src/main/kotlin/de/uapcore/lightpit/types/VcsType.kt	Sat Jul 22 22:32:04 2023 +0200
     7.3 @@ -0,0 +1,29 @@
     7.4 +/*
     7.5 + * Copyright 2023 Mike Becker. All rights reserved.
     7.6 + *
     7.7 + * Redistribution and use in source and binary forms, with or without
     7.8 + * modification, are permitted provided that the following conditions are met:
     7.9 + *
    7.10 + * 1. Redistributions of source code must retain the above copyright
    7.11 + * notice, this list of conditions and the following disclaimer.
    7.12 + *
    7.13 + * 2. Redistributions in binary form must reproduce the above copyright
    7.14 + * notice, this list of conditions and the following disclaimer in the
    7.15 + * documentation and/or other materials provided with the distribution.
    7.16 + *
    7.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    7.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    7.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    7.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
    7.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    7.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    7.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    7.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    7.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    7.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    7.27 + *
    7.28 + */
    7.29 +
    7.30 +package de.uapcore.lightpit.types
    7.31 +
    7.32 +enum class VcsType {None, Mercurial, Git}
    7.33 \ No newline at end of file
     8.1 --- a/src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt	Sat Jul 22 15:07:23 2023 +0200
     8.2 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt	Sat Jul 22 22:32:04 2023 +0200
     8.3 @@ -99,6 +99,8 @@
     8.4      }
     8.5  }
     8.6  
     8.7 +data class CommitLink(val url: String, val hash: String, val message: String)
     8.8 +
     8.9  class IssueDetailView(
    8.10      val issue: Issue,
    8.11      val comments: List<IssueComment>,
    8.12 @@ -110,10 +112,12 @@
    8.13      /**
    8.14       * Optional resource key to an error message for the relation editor.
    8.15       */
    8.16 -    val relationError: String?
    8.17 +    val relationError: String?,
    8.18 +    commitRefs: List<CommitRef>
    8.19  ) : View() {
    8.20      val relationTypes = RelationType.values()
    8.21      val linkableIssues = projectIssues.filterNot { it.id == issue.id }
    8.22 +    val commitLinks: List<CommitLink>
    8.23  
    8.24      private val parser: Parser
    8.25      private val renderer: HtmlRenderer
    8.26 @@ -131,8 +135,24 @@
    8.27          for (comment in comments) {
    8.28              comment.commentFormatted = formatMarkdown(comment.comment)
    8.29          }
    8.30 +
    8.31 +        val commitBaseUrl = project.repoUrl
    8.32 +        commitLinks = (if (commitBaseUrl == null || project.vcs == VcsType.None) emptyList() else commitRefs.map {
    8.33 +            CommitLink(buildCommitUrl(commitBaseUrl, project.vcs, it.hash), it.hash, it.message)
    8.34 +        })
    8.35      }
    8.36  
    8.37 +    private fun buildCommitUrl(baseUrl: String, vcs: VcsType, hash: String): String =
    8.38 +        with (StringBuilder(baseUrl)) {
    8.39 +            if (!endsWith("/")) append('/')
    8.40 +            when (vcs) {
    8.41 +                VcsType.Mercurial -> append("rev/")
    8.42 +                else -> append("commit/")
    8.43 +            }
    8.44 +            append(hash)
    8.45 +            toString()
    8.46 +        }
    8.47 +
    8.48      private fun formatEmojis(text: String) = text
    8.49          .replace("(/)", "&#9989;")
    8.50          .replace("(x)", "&#10060;")
     9.1 --- a/src/main/resources/localization/strings.properties	Sat Jul 22 15:07:23 2023 +0200
     9.2 +++ b/src/main/resources/localization/strings.properties	Sat Jul 22 22:32:04 2023 +0200
     9.3 @@ -83,6 +83,9 @@
     9.4  issue.comments.lastupdate=Last edited:
     9.5  issue.comments.updateCount=total edits
     9.6  issue.comments=Comments
     9.7 +issue.commits.hash=Hash
     9.8 +issue.commits.message=Brief
     9.9 +issue.commits=Commits
    9.10  issue.created=Created
    9.11  issue.description=Description
    9.12  issue.eta=ETA
    9.13 @@ -156,6 +159,8 @@
    9.14  project.name=Name
    9.15  project.owner=Project Lead
    9.16  project.repoUrl=Repository
    9.17 +project.vcs=Version Control
    9.18 +project.vcs.none=Do not analyze repository
    9.19  project=Project
    9.20  user.displayname=Developer
    9.21  user.givenname=Given Name
    10.1 --- a/src/main/resources/localization/strings_de.properties	Sat Jul 22 15:07:23 2023 +0200
    10.2 +++ b/src/main/resources/localization/strings_de.properties	Sat Jul 22 22:32:04 2023 +0200
    10.3 @@ -83,6 +83,9 @@
    10.4  issue.comments.lastupdate=Zuletzt bearbeitet:
    10.5  issue.comments.updateCount=mal bearbeitet
    10.6  issue.comments=Kommentare
    10.7 +issue.commits.hash=Hash
    10.8 +issue.commits.message=Zusammenfassung
    10.9 +issue.commits=Commits
   10.10  issue.created=Erstellt
   10.11  issue.description=Beschreibung
   10.12  issue.eta=Zieldatum
   10.13 @@ -156,6 +159,8 @@
   10.14  project.name=Name
   10.15  project.owner=Projektleitung
   10.16  project.repoUrl=Repository
   10.17 +project.vcs=Versionskontrolle
   10.18 +project.vcs.none=Keine Analyse durchf\u00fchren
   10.19  project=Projekt
   10.20  user.displayname=Entwickler
   10.21  user.givenname=Vorname
    11.1 --- a/src/main/webapp/WEB-INF/changelogs/changelog-de.jspf	Sat Jul 22 15:07:23 2023 +0200
    11.2 +++ b/src/main/webapp/WEB-INF/changelogs/changelog-de.jspf	Sat Jul 22 22:32:04 2023 +0200
    11.3 @@ -24,6 +24,12 @@
    11.4    --%>
    11.5  <%@ page contentType="text/html;charset=UTF-8" %>
    11.6  
    11.7 +<h3>Version 1.1.0</h3>
    11.8 +
    11.9 +<ul>
   11.10 +    <li>Integration von Commit-Logs für Mercurial und Git hinzugefügt.</li>
   11.11 +</ul>
   11.12 +
   11.13  <h3>Version 1.0.1</h3>
   11.14  
   11.15  <ul>
    12.1 --- a/src/main/webapp/WEB-INF/changelogs/changelog.jspf	Sat Jul 22 15:07:23 2023 +0200
    12.2 +++ b/src/main/webapp/WEB-INF/changelogs/changelog.jspf	Sat Jul 22 22:32:04 2023 +0200
    12.3 @@ -24,6 +24,12 @@
    12.4    --%>
    12.5  <%@ page contentType="text/html;charset=UTF-8" %>
    12.6  
    12.7 +<h3>Version 1.1.0</h3>
    12.8 +
    12.9 +<ul>
   12.10 +    <li>Add integration of mercurial and git commit logs.</li>
   12.11 +</ul>
   12.12 +
   12.13  <h3>Version 1.0.1</h3>
   12.14  
   12.15  <ul>
    13.1 --- a/src/main/webapp/WEB-INF/jsp/issue-view.jsp	Sat Jul 22 15:07:23 2023 +0200
    13.2 +++ b/src/main/webapp/WEB-INF/jsp/issue-view.jsp	Sat Jul 22 22:32:04 2023 +0200
    13.3 @@ -156,6 +156,31 @@
    13.4  </div>
    13.5  
    13.6  <hr class="issue-view-separator"/>
    13.7 +
    13.8 +<c:if test="${not empty viewmodel.commitLinks}">
    13.9 +    <h2><fmt:message key="issue.commits" /></h2>
   13.10 +    <table class="issue-view fullwidth">
   13.11 +        <colgroup>
   13.12 +            <col>
   13.13 +            <col class="fullwidth">
   13.14 +        </colgroup>
   13.15 +        <thead>
   13.16 +        <tr>
   13.17 +            <th><fmt:message key="issue.commits.hash"/></th>
   13.18 +            <th><fmt:message key="issue.commits.message"/></th>
   13.19 +        </tr>
   13.20 +        </thead>
   13.21 +        <tbody>
   13.22 +        <c:forEach var="commitLink" items="${viewmodel.commitLinks}">
   13.23 +        <tr>
   13.24 +            <td><a href="${commitLink.url}" target="_blank">${commitLink.hash}</a></td>
   13.25 +            <td><c:out value="${commitLink.message}"/> </td>
   13.26 +        </tr>
   13.27 +        </c:forEach>
   13.28 +        </tbody>
   13.29 +    </table>
   13.30 +</c:if>
   13.31 +
   13.32  <h2>
   13.33      <fmt:message key="issue.relations"/>
   13.34  </h2>
    14.1 --- a/src/main/webapp/WEB-INF/jsp/project-form.jsp	Sat Jul 22 15:07:23 2023 +0200
    14.2 +++ b/src/main/webapp/WEB-INF/jsp/project-form.jsp	Sat Jul 22 22:32:04 2023 +0200
    14.3 @@ -55,6 +55,17 @@
    14.4              <td><input name="repoUrl" type="url" maxlength="50" value="<c:out value="${project.repoUrl}"/>" /></td>
    14.5          </tr>
    14.6          <tr>
    14.7 +            <th><fmt:message key="project.vcs"/></th>
    14.8 +            <td>
    14.9 +                <select name="vcs">
   14.10 +                    <option value="None"><fmt:message key="project.vcs.none"/></option>
   14.11 +                    <c:forTokens var="vcs" items="Mercurial,Git" delims=",">
   14.12 +                    <option <c:if test="${project.vcs eq vcs}">selected</c:if> value="${vcs}">${vcs}</option>
   14.13 +                    </c:forTokens>
   14.14 +                </select>
   14.15 +            </td>
   14.16 +        </tr>
   14.17 +        <tr>
   14.18              <th><fmt:message key="project.owner"/></th>
   14.19              <td>
   14.20                  <select name="owner">

mercurial