Merge pull request #318 from movableink/rebuild-commits-v2

Rebuild commits
This commit is contained in:
Brad Rydzewski 2014-05-28 13:58:33 -07:00
commit c8a7b819cd
17 changed files with 198 additions and 113 deletions

View file

@ -870,6 +870,14 @@ pre {
font-size: 22px !IMPORTANT;
line-height: 32px !IMPORTANT;
}
.alert.alert-build-Success .actions,
.alert.alert-build-Error .actions,
.alert.alert-build-Failure .actions,
.alert.alert-build-Pending .actions,
.alert.alert-build-Started .actions {
float: right;
margin-top: -2px;
}
.build-details {
background: #FFF;
margin-bottom: 40px;

View file

@ -184,7 +184,7 @@ body {
border:0px;
}
//
//
// nav-repos
// --------------------------------------------
@ -364,7 +364,7 @@ body {
margin-bottom: 10px;
}
//
//
// build list
// --------------------------------------------
@ -493,7 +493,7 @@ body {
.btn.btn-Success {
background:rgba(81, 163, 81, 0.75);
}
.btn.btn-failure,
.btn.btn-failure,
.btn.btn-Failure,
.btn.btn-Error {
background:rgba(189, 54, 47, 0.8);
@ -911,7 +911,7 @@ textarea {
top: 6px;
transition: all .15s ease;
width: 35px;
z-index: 3;
z-index: 3;
border-radius:5px;
}
@ -1021,6 +1021,11 @@ pre {
line-height: 32px !IMPORTANT;
}
}
.actions {
float: right;
margin-top: -2px;
}
}
.build-details {
@ -1285,5 +1290,3 @@ pre {
background: #999;
cursor: pointer;
}

View file

@ -136,6 +136,7 @@ func setupHandlers() {
github = handler.NewGithubHandler(queue)
gitlab = handler.NewGitlabHandler(queue)
bitbucket = handler.NewBitbucketHandler(queue)
rebuild = handler.NewCommitRebuildHandler(queue)
)
m := pat.New()
@ -226,7 +227,9 @@ func setupHandlers() {
// handlers for repository, commits and build details
m.Get("/:host/:owner/:name/commit/:commit/build/:label/out.txt", handler.RepoHandler(handler.BuildOut))
m.Post("/:host/:owner/:name/commit/:commit/build/:label/rebuild", handler.RepoAdminHandler(rebuild.CommitRebuild))
m.Get("/:host/:owner/:name/commit/:commit/build/:label", handler.RepoHandler(handler.CommitShow))
m.Post("/:host/:owner/:name/commit/:commit/rebuild", handler.RepoAdminHandler(rebuild.CommitRebuild))
m.Get("/:host/:owner/:name/commit/:commit", handler.RepoHandler(handler.CommitShow))
m.Get("/:host/:owner/:name/tree", handler.RepoHandler(handler.RepoDashboard))
m.Get("/:host/:owner/:name/status.svg", handler.ErrorHandler(handler.Badge))

View file

@ -10,7 +10,7 @@ const buildTable = "builds"
// SQL Queries to retrieve a list of all Commits belonging to a Repo.
const buildStmt = `
SELECT id, commit_id, slug, status, started, finished, duration, created, updated, stdout
SELECT id, commit_id, slug, status, started, finished, duration, created, updated, stdout, buildscript
FROM builds
WHERE commit_id = ?
ORDER BY slug ASC
@ -18,7 +18,7 @@ ORDER BY slug ASC
// SQL Queries to retrieve a Build by id.
const buildFindStmt = `
SELECT id, commit_id, slug, status, started, finished, duration, created, updated, stdout
SELECT id, commit_id, slug, status, started, finished, duration, created, updated, stdout, buildscript
FROM builds
WHERE id = ?
LIMIT 1
@ -26,7 +26,7 @@ LIMIT 1
// SQL Queries to retrieve a Commit by name and repo id.
const buildFindSlugStmt = `
SELECT id, commit_id, slug, status, started, finished, duration, created, updated, stdout
SELECT id, commit_id, slug, status, started, finished, duration, created, updated, stdout, buildscript
FROM builds
WHERE slug = ? AND commit_id = ?
LIMIT 1

View file

@ -0,0 +1,19 @@
package migrate
type rev20140522205400 struct{}
var SaveDroneYml = &rev20140522205400{}
func (r *rev20140522205400) Revision() int64 {
return 20140522205400
}
func (r *rev20140522205400) Up(mg *MigrationDriver) error {
_, err := mg.AddColumn("builds", "buildscript TEXT")
return err
}
func (r *rev20140522205400) Down(mg *MigrationDriver) error {
_, err := mg.DropColumns("builds", "buildscript")
return err
}

View file

@ -13,6 +13,7 @@ func (m *Migration) All() *Migration {
m.Add(GitHubEnterpriseSupport)
m.Add(AddOpenInvitationColumn)
m.Add(AddGitlabColumns)
m.Add(SaveDroneYml)
// m.Add(...)
// ...

View file

@ -90,3 +90,18 @@ func ListReposTeam(id int64) ([]*Repo, error) {
err := meddler.QueryAll(db, &repos, repoTeamStmt, id)
return repos, err
}
// Checks whether a user is admin of a repo
// Returns true if user owns repo or is on team that owns repo
// Returns true if the user is an admin member of the team.
func IsRepoAdmin(user *User, repo *Repo) (bool, error) {
if user == nil {
return false, nil
}
if user.ID == repo.UserID {
return true, nil
}
return IsMemberAdmin(user.ID, repo.TeamID)
}

View file

@ -5,7 +5,6 @@ import (
"net/http"
"time"
"github.com/drone/drone/pkg/build/script"
"github.com/drone/drone/pkg/database"
. "github.com/drone/drone/pkg/model"
"github.com/drone/drone/pkg/queue"
@ -83,16 +82,6 @@ func (h *BitbucketHandler) Hook(w http.ResponseWriter, r *http.Request) error {
return RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
}
// parse the build script
buildscript, err := script.ParseBuild([]byte(raw.Data), repo.Params)
if err != nil {
msg := "Could not parse your .drone.yml file. It needs to be a valid drone yaml file.\n\n" + err.Error() + "\n"
if err := saveFailedBuild(commit, msg); err != nil {
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
}
// save the commit to the database
if err := database.SaveCommit(commit); err != nil {
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -104,12 +93,13 @@ func (h *BitbucketHandler) Hook(w http.ResponseWriter, r *http.Request) error {
build.CommitID = commit.ID
build.Created = time.Now().UTC()
build.Status = "Pending"
build.BuildScript = raw.Data
if err := database.SaveBuild(build); err != nil {
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
// send the build to the queue
h.queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build, Script: buildscript})
h.queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build})
// OK!
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)

View file

@ -8,6 +8,7 @@ import (
"github.com/drone/drone/pkg/channel"
"github.com/drone/drone/pkg/database"
. "github.com/drone/drone/pkg/model"
"github.com/drone/drone/pkg/queue"
)
// Display a specific Commit.
@ -33,14 +34,20 @@ func CommitShow(w http.ResponseWriter, r *http.Request, u *User, repo *Repo) err
return err
}
admin, err := database.IsRepoAdmin(u, repo)
if err != nil {
return err
}
data := struct {
User *User
Repo *Repo
Commit *Commit
Build *Build
Builds []*Build
Token string
}{u, repo, commit, builds[0], builds, ""}
User *User
Repo *Repo
Commit *Commit
Build *Build
Builds []*Build
Token string
IsAdmin bool
}{u, repo, commit, builds[0], builds, "", admin}
// get the specific build requested by the user. instead
// of a database round trip, we can just loop through the
@ -94,3 +101,70 @@ func saveFailedBuild(commit *Commit, msg string) error {
return nil
}
type CommitRebuildHandler struct {
queue *queue.Queue
}
func NewCommitRebuildHandler(queue *queue.Queue) *CommitRebuildHandler {
return &CommitRebuildHandler{
queue: queue,
}
}
// CommitRebuild re-queues a previously built commit. It finds the existing
// commit and build and injects them back into the queue. If the commit
// doesn't exist or has no builds, or if a build label has been passed but
// can't be located, it prints an error. Otherwise, it adds the build/commit
// to the queue and redirects back to the commit page.
func (h *CommitRebuildHandler) CommitRebuild(w http.ResponseWriter, r *http.Request, u *User, repo *Repo) error {
hash := r.FormValue(":commit")
labl := r.FormValue(":label")
host := r.FormValue(":host")
branch := r.FormValue("branch")
if branch == "" {
branch = "master"
}
// get the commit from the database
commit, err := database.GetCommitBranchHash(branch, hash, repo.ID)
if err != nil {
return err
}
// get the builds from the database. a commit can have
// multiple sub-builds (or matrix builds)
builds, err := database.ListBuilds(commit.ID)
if err != nil {
return err
}
build := builds[0]
if labl != "" {
// get the specific build requested by the user. instead
// of a database round trip, we can just loop through the
// list and extract the requested build.
build = nil
for _, b := range builds {
if b.Slug == labl {
build = b
break
}
}
}
if build == nil {
return fmt.Errorf("Could not find build: %s", labl)
}
h.queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build})
if labl != "" {
http.Redirect(w, r, fmt.Sprintf("/%s/%s/%s/commit/%s/build/%s?branch=%s", host, repo.Owner, repo.Name, hash, labl, branch), http.StatusSeeOther)
} else {
http.Redirect(w, r, fmt.Sprintf("/%s/%s/%s/commit/%s?branch=%s", host, repo.Owner, repo.Name, hash, branch), http.StatusSeeOther)
}
return nil
}

View file

@ -6,7 +6,6 @@ import (
"strconv"
"time"
"github.com/drone/drone/pkg/build/script"
"github.com/drone/drone/pkg/database"
. "github.com/drone/drone/pkg/model"
"github.com/drone/drone/pkg/queue"
@ -131,22 +130,8 @@ func (h *GithubHandler) Hook(w http.ResponseWriter, r *http.Request) error {
}
// decode the content. Note: Not sure this will ever happen...it basically means a GitHub API issue
raw, err := content.DecodeContent()
buildscript, err := content.DecodeContent()
if err != nil {
msg := "Could not decode the yaml from GitHub. Check that your .drone.yml is a valid yaml file.\n"
if err := saveFailedBuild(commit, msg); err != nil {
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
}
// parse the build script
buildscript, err := script.ParseBuild(raw, repo.Params)
if err != nil {
msg := "Could not parse your .drone.yml file. It needs to be a valid drone yaml file.\n\n" + err.Error() + "\n"
if err := saveFailedBuild(commit, msg); err != nil {
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
}
@ -161,6 +146,7 @@ func (h *GithubHandler) Hook(w http.ResponseWriter, r *http.Request) error {
build.CommitID = commit.ID
build.Created = time.Now().UTC()
build.Status = "Pending"
build.BuildScript = string(buildscript)
if err := database.SaveBuild(build); err != nil {
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
@ -169,7 +155,7 @@ func (h *GithubHandler) Hook(w http.ResponseWriter, r *http.Request) error {
//realtime.CommitPending(repo.UserID, repo.TeamID, repo.ID, commit.ID, repo.Private)
//realtime.BuildPending(repo.UserID, repo.TeamID, repo.ID, commit.ID, build.ID, repo.Private)
h.queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build, Script: buildscript}) //Push(repo, commit, build, buildscript)
h.queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build}) //Push(repo, commit, build)
// OK!
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
@ -250,22 +236,12 @@ func (h *GithubHandler) PullRequestHook(w http.ResponseWriter, r *http.Request)
}
// decode the content
raw, err := content.DecodeContent()
buildscript, err := content.DecodeContent()
if err != nil {
RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
// parse the build script
buildscript, err := script.ParseBuild(raw, repo.Params)
if err != nil {
// TODO if the YAML is invalid we should create a commit record
// with an ERROR status so that the user knows why a build wasn't
// triggered in the system
RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
// save the commit to the database
if err := database.SaveCommit(commit); err != nil {
RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -278,6 +254,7 @@ func (h *GithubHandler) PullRequestHook(w http.ResponseWriter, r *http.Request)
build.CommitID = commit.ID
build.Created = time.Now().UTC()
build.Status = "Pending"
build.BuildScript = string(buildscript)
if err := database.SaveBuild(build); err != nil {
RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
@ -285,7 +262,7 @@ func (h *GithubHandler) PullRequestHook(w http.ResponseWriter, r *http.Request)
// notify websocket that a new build is pending
// TODO we should, for consistency, just put this inside Queue.Add()
h.queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build, Script: buildscript})
h.queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build})
// OK!
RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)

View file

@ -9,7 +9,6 @@ import (
"strings"
"time"
"github.com/drone/drone/pkg/build/script"
"github.com/drone/drone/pkg/database"
. "github.com/drone/drone/pkg/model"
"github.com/drone/drone/pkg/queue"
@ -207,7 +206,7 @@ func (g *GitlabHandler) Hook(w http.ResponseWriter, r *http.Request) error {
// get the drone.yml file from GitHub
client := gogitlab.NewGitlab(settings.GitlabApiUrl, g.apiPath, user.GitlabToken)
content, err := client.RepoRawFile(ns(repo.Owner, repo.Name), commit.Hash, ".drone.yml")
buildscript, err := client.RepoRawFile(ns(repo.Owner, repo.Name), commit.Hash, ".drone.yml")
if err != nil {
msg := "No .drone.yml was found in this repository. You need to add one.\n"
if err := saveFailedBuild(commit, msg); err != nil {
@ -216,16 +215,6 @@ func (g *GitlabHandler) Hook(w http.ResponseWriter, r *http.Request) error {
return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
}
// parse the build script
buildscript, err := script.ParseBuild(content, repo.Params)
if err != nil {
msg := "Could not parse your .drone.yml file. It needs to be a valid drone yaml file.\n\n" + err.Error() + "\n"
if err := saveFailedBuild(commit, msg); err != nil {
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
}
// save the commit to the database
if err := database.SaveCommit(commit); err != nil {
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -237,11 +226,12 @@ func (g *GitlabHandler) Hook(w http.ResponseWriter, r *http.Request) error {
build.CommitID = commit.ID
build.Created = time.Now().UTC()
build.Status = "Pending"
build.BuildScript = string(buildscript)
if err := database.SaveBuild(build); err != nil {
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
g.queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build, Script: buildscript})
g.queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build})
// OK!
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
@ -287,7 +277,7 @@ func (g *GitlabHandler) PullRequestHook(p *gogitlab.HookPayload, repo *Repo, use
commit.Timestamp = src.Commit.AuthoredDateRaw
commit.SetAuthor(src.Commit.Author.Email)
content, err := client.RepoRawFile(strconv.Itoa(obj.SourceProjectId), commit.Hash, ".drone.yml")
buildscript, err := client.RepoRawFile(strconv.Itoa(obj.SourceProjectId), commit.Hash, ".drone.yml")
if err != nil {
msg := "No .drone.yml was found in this repository. You need to add one.\n"
if err := saveFailedBuild(commit, msg); err != nil {
@ -296,16 +286,6 @@ func (g *GitlabHandler) PullRequestHook(p *gogitlab.HookPayload, repo *Repo, use
return fmt.Errorf("Error to fetch build script: %q", err)
}
// parse the build script
buildscript, err := script.ParseBuild(content, repo.Params)
if err != nil {
msg := "Could not parse your .drone.yml file. It needs to be a valid drone yaml file.\n\n" + err.Error() + "\n"
if err := saveFailedBuild(commit, msg); err != nil {
return fmt.Errorf("Failed to save build: %q", err)
}
return fmt.Errorf("Failed to parse build script: %q", err)
}
// save the commit to the database
if err := database.SaveCommit(commit); err != nil {
return fmt.Errorf("Failed to save commit: %q", err)
@ -317,11 +297,12 @@ func (g *GitlabHandler) PullRequestHook(p *gogitlab.HookPayload, repo *Repo, use
build.CommitID = commit.ID
build.Created = time.Now().UTC()
build.Status = "Pending"
build.BuildScript = string(buildscript)
if err := database.SaveBuild(build); err != nil {
return fmt.Errorf("Failed to save build: %q", err)
}
g.queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build, Script: buildscript})
g.queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build})
return nil
}

View file

@ -142,11 +142,9 @@ func (h RepoAdminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// The User must own the repository OR be a member
// of the Team that owns the repository.
if user.ID != repo.UserID {
if admin, _ := database.IsMemberAdmin(user.ID, repo.TeamID); admin == false {
RenderNotFound(w)
return
}
if admin, _ := database.IsRepoAdmin(user, repo); admin == false {
RenderNotFound(w)
return
}
if err = h(w, r, user, repo); err != nil {

View file

@ -45,7 +45,7 @@ func TestRepoHandler(t *testing.T) {
So(rec.Code, ShouldEqual, 303)
})
Convey("Private repo can not be viewed by a non team member", func() {
req, err := http.NewRequest("GET", "/github.com/drone/drone", nil)
req, err := http.NewRequest("GET", "/github.com/drone/drone", nil)
So(err, ShouldBeNil)
rec := httptest.NewRecorder()
setUserSession(rec, req, "rick@el.to.ro")
@ -61,7 +61,7 @@ func dummyUserRepo(w http.ResponseWriter, r *http.Request, u *User, repo *Repo)
func setUserSession(w http.ResponseWriter, r *http.Request, username string) {
handler.SetCookie(w, r, "_sess", username)
resp := http.Response{Header: w.Header()}
resp := http.Response{Header: w.Header()}
for _, v := range resp.Cookies() {
r.AddCookie(v)
}

View file

@ -15,16 +15,17 @@ const (
)
type Build struct {
ID int64 `meddler:"id,pk" json:"id"`
CommitID int64 `meddler:"commit_id" json:"-"`
Slug string `meddler:"slug" json:"slug"`
Status string `meddler:"status" json:"status"`
Started time.Time `meddler:"started,utctime" json:"started"`
Finished time.Time `meddler:"finished,utctime" json:"finished"`
Duration int64 `meddler:"duration" json:"duration"`
Created time.Time `meddler:"created,utctime" json:"created"`
Updated time.Time `meddler:"updated,utctime" json:"updated"`
Stdout string `meddler:"stdout" json:"-"`
ID int64 `meddler:"id,pk" json:"id"`
CommitID int64 `meddler:"commit_id" json:"-"`
Slug string `meddler:"slug" json:"slug"`
Status string `meddler:"status" json:"status"`
Started time.Time `meddler:"started,utctime" json:"started"`
Finished time.Time `meddler:"finished,utctime" json:"finished"`
Duration int64 `meddler:"duration" json:"duration"`
Created time.Time `meddler:"created,utctime" json:"created"`
Updated time.Time `meddler:"updated,utctime" json:"updated"`
Stdout string `meddler:"stdout" json:"-"`
BuildScript string `meddler:"buildscript" json:"-"`
}
// HumanDuration returns a human-readable approximation of a duration

View file

@ -1,7 +1,6 @@
package queue
import (
"github.com/drone/drone/pkg/build/script"
. "github.com/drone/drone/pkg/model"
)
@ -16,10 +15,6 @@ type BuildTask struct {
Repo *Repo
Commit *Commit
Build *Build
// Build instructions from the .drone.yml
// file, unmarshalled.
Script *script.Build
}
// Start N workers with the given build runner.

View file

@ -5,6 +5,7 @@ import (
"fmt"
"github.com/drone/drone/pkg/build/git"
r "github.com/drone/drone/pkg/build/repo"
"github.com/drone/drone/pkg/build/script"
"github.com/drone/drone/pkg/channel"
"github.com/drone/drone/pkg/database"
. "github.com/drone/drone/pkg/model"
@ -83,9 +84,16 @@ func (w *worker) execute(task *BuildTask) error {
Host: settings.URL().String(),
}
// parse the build script
buildscript, err := script.ParseBuild([]byte(task.Build.BuildScript), task.Repo.Params)
if err != nil {
log.Printf("Could not parse your .drone.yml file. It needs to be a valid drone yaml file.\n\n" + err.Error() + "\n")
return err
}
// send all "started" notifications
if task.Script.Notifications != nil {
task.Script.Notifications.Send(context)
if buildscript.Notifications != nil {
buildscript.Notifications.Send(context)
}
// Send "started" notification to Github
@ -113,7 +121,7 @@ func (w *worker) execute(task *BuildTask) error {
// this is not a pull request (for security purposes)
if task.Repo.Params != nil && len(task.Commit.PullRequest) == 0 {
for k, v := range task.Repo.Params {
task.Script.Env = append(task.Script.Env, k+"="+v)
buildscript.Env = append(buildscript.Env, k+"="+v)
}
}
@ -126,7 +134,7 @@ func (w *worker) execute(task *BuildTask) error {
}()
// execute the build
passed, buildErr := w.runBuild(task, buf)
passed, buildErr := w.runBuild(task, buildscript, buf)
task.Build.Finished = time.Now().UTC()
task.Commit.Finished = time.Now().UTC()
@ -162,14 +170,14 @@ func (w *worker) execute(task *BuildTask) error {
channel.Close(consoleslug)
// send all "finished" notifications
if task.Script.Notifications != nil {
task.Script.Notifications.Send(context)
if buildscript.Notifications != nil {
buildscript.Notifications.Send(context)
}
return nil
}
func (w *worker) runBuild(task *BuildTask, buf io.Writer) (bool, error) {
func (w *worker) runBuild(task *BuildTask, buildscript *script.Build, buf io.Writer) (bool, error) {
repo := &r.Repo{
Name: task.Repo.Slug,
Path: task.Repo.URL,
@ -177,11 +185,11 @@ func (w *worker) runBuild(task *BuildTask, buf io.Writer) (bool, error) {
Commit: task.Commit.Hash,
PR: task.Commit.PullRequest,
Dir: filepath.Join("/var/cache/drone/src", task.Repo.Slug),
Depth: git.GitDepth(task.Script.Git),
Depth: git.GitDepth(buildscript.Git),
}
return w.runner.Run(
task.Script,
buildscript,
repo,
[]byte(task.Repo.PrivateKey),
task.Repo.Privileged,

View file

@ -24,6 +24,18 @@
{{ else }}
<span>commit <span>{{ .Commit.HashShort }}</span> to <span>{{.Commit.Branch}}</span> branch</span>
{{ end }}
<div class="actions">
{{ if .IsAdmin }}
{{ if not .Build.IsRunning }}
<form action="/{{.Repo.Slug}}/commit/{{.Commit.Hash}}/rebuild?branch={{ .Commit.Branch }}"
method="POST">
<input class="btn btn-default" type="submit" value="Rebuild"/>
</form>
{{ end }}
{{ end }}
</div>
</div>
<div class="build-details container affix-top" data-spy="affix" data-offset-top="248">
<div class="build-summary">