Fix Bitbucket merging conflict

This commit is contained in:
Nurahmadie 2014-03-29 06:49:44 +00:00
commit ea4917e278
17 changed files with 457 additions and 17 deletions

View file

@ -57,7 +57,7 @@ js:
cd cmd/droned/assets && find js -name "*.js" ! -name '.*' ! -name "main.js" -exec cat {} \; > js/main.js
build:
cd cmd/drone && go build -o ../../bin/drone
cd cmd/drone && go build -ldflags "-X main.version $(SHA)" -o ../../bin/drone
cd cmd/droned && go build -ldflags "-X main.version $(SHA)" -o ../../bin/droned
test: $(PKGS)

View file

@ -39,6 +39,10 @@ var (
// displays the help / usage if True
help = flag.Bool("h", false, "")
// version number, currently deterined by the
// git revision number (sha)
version string
)
func init() {
@ -103,6 +107,10 @@ func main() {
path = filepath.Join(path, ".drone.yml")
vet(path)
// print the version / revision number
case args[0] == "version" && len(args) == 1:
println(version)
// print the help message
case args[0] == "help" && len(args) == 1:
flag.Usage()

View file

@ -134,11 +134,18 @@ func setupHandlers() {
// handlers for setting up your GitHub repository
m.Post("/new/github.com", handler.UserHandler(handler.RepoCreateGithub))
m.Get("/new/github.com", handler.UserHandler(handler.RepoAdd))
m.Get("/new/github.com", handler.UserHandler(handler.RepoAddGithub))
// handlers for linking your GitHub account
m.Get("/auth/login/github", handler.UserHandler(handler.LinkGithub))
// handlers for setting up your Bitbucket repository
m.Post("/new/bitbucket.org", handler.UserHandler(handler.RepoCreateBitbucket))
m.Get("/new/bitbucket.org", handler.UserHandler(handler.RepoAddBitbucket))
// handlers for linking your Bitbucket account
m.Get("/auth/login/bitbucket", handler.UserHandler(handler.LinkBitbucket))
// handlers for setting up your GitLab repository
m.Post("/new/gitlab", handler.UserHandler(gitlab.Create))
m.Get("/new/gitlab", handler.UserHandler(gitlab.Add))
@ -184,7 +191,10 @@ func setupHandlers() {
m.Get("/account/admin/users", handler.AdminHandler(handler.AdminUserList))
// handlers for GitHub post-commit hooks
m.Post("/hook/github.com", handler.ErrorHandler(hookHandler.Hook))
m.Post("/hook/github.com", handler.ErrorHandler(hookHandler.HookGithub))
// handlers for Bitbucket post-commit hooks
m.Post("/hook/bitbucket.org", handler.ErrorHandler(hookHandler.HookBitbucket))
// handlers for GitLab post-commit hooks
//m.Post("/hook/gitlab", handler.ErrorHandler(gitlab.Hook))

View file

@ -491,6 +491,14 @@ func (b *Builder) writeBuildScript(dir string) error {
f.WriteEnv("DRONE_PR", b.Repo.PR)
f.WriteEnv("DRONE_BUILD_DIR", b.Repo.Dir)
// add environment variables for code coverage
// systems, like coveralls.
f.WriteEnv("CI_NAME", "DRONE")
f.WriteEnv("CI_BUILD_NUMBER", b.Repo.Commit)
f.WriteEnv("CI_BUILD_URL", "")
f.WriteEnv("CI_BRANCH", b.Repo.Branch)
f.WriteEnv("CI_PULL_REQUEST", b.Repo.PR)
// add /etc/hosts entries
for _, mapping := range b.Build.Hosts {
f.WriteHost(mapping)

View file

@ -562,6 +562,11 @@ func TestWriteBuildScript(t *testing.T) {
f.WriteEnv("DRONE_COMMIT", "e7e046b35")
f.WriteEnv("DRONE_PR", "123")
f.WriteEnv("DRONE_BUILD_DIR", "/var/cache/drone/github.com/drone/drone")
f.WriteEnv("CI_NAME", "DRONE")
f.WriteEnv("CI_BUILD_NUMBER", "e7e046b35")
f.WriteEnv("CI_BUILD_URL", "")
f.WriteEnv("CI_BRANCH", "master")
f.WriteEnv("CI_PULL_REQUEST", "123")
f.WriteHost("127.0.0.1")
f.WriteCmd("git clone --depth=0 --recursive --branch=master git://github.com/drone/drone.git /var/cache/drone/github.com/drone/drone")
f.WriteCmd("git fetch origin +refs/pull/123/head:refs/remotes/origin/pr/123")

View file

@ -21,7 +21,8 @@ func MySQL(tx *sql.Tx) *MigrationDriver {
}
func (m *mysqlDriver) CreateTable(tableName string, args []string) (sql.Result, error) {
return m.Tx.Exec(fmt.Sprintf("CREATE TABLE %s (%s) ROW_FORMAT=DYNAMIC", tableName, strings.Join(args, ", ")))
return m.Tx.Exec(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (%s) ROW_FORMAT=DYNAMIC",
tableName, strings.Join(args, ", ")))
}
func (m *mysqlDriver) RenameTable(tableName, newName string) (sql.Result, error) {
@ -41,6 +42,9 @@ func (m *mysqlDriver) ChangeColumn(tableName, columnName, newSpecs string) (sql.
}
func (m *mysqlDriver) DropColumns(tableName string, columnsToDrop ...string) (sql.Result, error) {
if len(columnsToDrop) == 0 {
return nil, fmt.Errorf("No columns to drop.")
}
for k, v := range columnsToDrop {
columnsToDrop[k] = fmt.Sprintf("DROP %s", v)
}

View file

@ -8,6 +8,8 @@ import (
. "github.com/drone/drone/pkg/model"
"github.com/drone/go-github/github"
"github.com/drone/go-github/oauth2"
"github.com/drone/go-bitbucket/bitbucket"
"github.com/drone/go-bitbucket/oauth1"
)
// Create the User session.
@ -94,3 +96,78 @@ func LinkGithub(w http.ResponseWriter, r *http.Request, u *User) error {
http.Redirect(w, r, "/new/github.com", http.StatusSeeOther)
return nil
}
func LinkBitbucket(w http.ResponseWriter, r *http.Request, u *User) error {
// get settings from database
settings := database.SettingsMust()
// bitbucket oauth1 consumer
var consumer = oauth1.Consumer{
RequestTokenURL: "https://bitbucket.org/api/1.0/oauth/request_token/",
AuthorizationURL: "https://bitbucket.org/!api/1.0/oauth/authenticate",
AccessTokenURL: "https://bitbucket.org/api/1.0/oauth/access_token/",
CallbackURL: settings.URL().String() + "/auth/login/bitbucket",
ConsumerKey: settings.BitbucketKey,
ConsumerSecret: settings.BitbucketSecret,
}
// get the oauth verifier
verifier := r.FormValue("oauth_verifier")
if len(verifier) == 0 {
// Generate a Request Token
requestToken, err := consumer.RequestToken()
if err != nil {
return err
}
// add the request token as a signed cookie
SetCookie(w, r, "bitbucket_token", requestToken.Encode())
url, _ := consumer.AuthorizeRedirect(requestToken)
http.Redirect(w, r, url, http.StatusSeeOther)
return nil
}
// remove bitbucket token data once before redirecting
// back to the application.
defer DelCookie(w, r, "bitbucket_token")
// get the tokens from the request
requestTokenStr := GetCookie(r, "bitbucket_token")
requestToken, err := oauth1.ParseRequestTokenStr(requestTokenStr)
if err != nil {
return err
}
// exchange for an access token
accessToken, err := consumer.AuthorizeToken(requestToken, verifier)
if err != nil {
return err
}
// create the Bitbucket client
client := bitbucket.New(
settings.BitbucketKey,
settings.BitbucketSecret,
accessToken.Token(),
accessToken.Secret(),
)
// get the currently authenticated Bitbucket User
user, err := client.Users.Current()
if err != nil {
return err
}
// update the user account
u.BitbucketLogin = user.User.Username
u.BitbucketToken = accessToken.Token()
u.BitbucketSecret = accessToken.Secret()
if err := database.SaveUser(u); err != nil {
return err
}
http.Redirect(w, r, "/new/bitbucket.org", http.StatusSeeOther)
return nil
}

View file

@ -11,6 +11,7 @@ import (
. "github.com/drone/drone/pkg/model"
"github.com/drone/drone/pkg/queue"
"github.com/drone/go-github/github"
"github.com/drone/go-bitbucket/bitbucket"
)
type HookHandler struct {
@ -23,9 +24,9 @@ func NewHookHandler(queue *queue.Queue) *HookHandler {
}
}
// Processes a generic POST-RECEIVE hook and
// Processes a generic POST-RECEIVE GitHub hook and
// attempts to trigger a build.
func (h *HookHandler) Hook(w http.ResponseWriter, r *http.Request) error {
func (h *HookHandler) HookGithub(w http.ResponseWriter, r *http.Request) error {
// handle github ping
if r.Header.Get("X-Github-Event") == "ping" {
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
@ -34,7 +35,7 @@ func (h *HookHandler) Hook(w http.ResponseWriter, r *http.Request) error {
// if this is a pull request route
// to a different handler
if r.Header.Get("X-Github-Event") == "pull_request" {
h.PullRequestHook(w, r)
h.PullRequestHookGithub(w, r)
return nil
}
@ -175,7 +176,7 @@ func (h *HookHandler) Hook(w http.ResponseWriter, r *http.Request) error {
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
}
func (h *HookHandler) PullRequestHook(w http.ResponseWriter, r *http.Request) {
func (h *HookHandler) PullRequestHookGithub(w http.ResponseWriter, r *http.Request) {
// get the payload of the message
// this should contain a json representation of the
// repository and commit details
@ -291,6 +292,99 @@ func (h *HookHandler) PullRequestHook(w http.ResponseWriter, r *http.Request) {
RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
}
// Processes a generic POST-RECEIVE Bitbucket hook and
// attempts to trigger a build.
func (h *HookHandler) HookBitbucket(w http.ResponseWriter, r *http.Request) error {
// get the payload from the request
payload := r.FormValue("payload")
// parse the post-commit hook
hook, err := bitbucket.ParseHook([]byte(payload))
if err != nil {
return err
}
// get the repo from the URL
repoId := r.FormValue("id")
// get the repo from the database, return error if not found
repo, err := database.GetRepoSlug(repoId)
if err != nil {
return RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
}
// Get the user that owns the repository
user, err := database.GetUser(repo.UserID)
if err != nil {
return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
}
// Verify that the commit doesn't already exist.
// We should never build the same commit twice.
_, err = database.GetCommitHash(hook.Commits[len(hook.Commits)-1].Hash, repo.ID)
if err != nil && err != sql.ErrNoRows {
return RenderText(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway)
}
commit := &Commit{}
commit.RepoID = repo.ID
commit.Branch = hook.Commits[len(hook.Commits)-1].Branch
commit.Hash = hook.Commits[len(hook.Commits)-1].Hash
commit.Status = "Pending"
commit.Created = time.Now().UTC()
commit.Message = hook.Commits[len(hook.Commits)-1].Message
commit.Timestamp = time.Now().UTC().String()
commit.SetAuthor(hook.Commits[len(hook.Commits)-1].Author)
// get the github settings from the database
settings := database.SettingsMust()
// create the Bitbucket client
client := bitbucket.New(
settings.BitbucketKey,
settings.BitbucketSecret,
user.BitbucketToken,
user.BitbucketSecret,
)
// get the yaml from the database
raw, err := client.Sources.Find(repo.Owner, repo.Name, commit.Hash, ".drone.yml")
if err != nil {
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)
}
// save the build to the database
build := &Build{}
build.Slug = "1" // TODO
build.CommitID = commit.ID
build.Created = time.Now().UTC()
build.Status = "Pending"
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})
// OK!
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
}
// Helper method for saving a failed build or commit in the case where it never starts to build.
// This can happen if the yaml is bad or doesn't exist.
func saveFailedBuild(commit *Commit, msg string) error {

View file

@ -8,6 +8,7 @@ import (
"github.com/drone/drone/pkg/database"
. "github.com/drone/drone/pkg/model"
"github.com/drone/go-github/github"
"github.com/drone/go-bitbucket/bitbucket"
"launchpad.net/goyaml"
)
@ -52,7 +53,7 @@ func RepoDashboard(w http.ResponseWriter, r *http.Request, u *User, repo *Repo)
return RenderTemplate(w, "repo_dashboard.html", &data)
}
func RepoAdd(w http.ResponseWriter, r *http.Request, u *User) error {
func RepoAddGithub(w http.ResponseWriter, r *http.Request, u *User) error {
settings := database.SettingsMust()
teams, err := database.ListTeams(u.ID)
if err != nil {
@ -73,6 +74,27 @@ func RepoAdd(w http.ResponseWriter, r *http.Request, u *User) error {
return RenderTemplate(w, "github_add.html", &data)
}
func RepoAddBitbucket(w http.ResponseWriter, r *http.Request, u *User) error {
settings := database.SettingsMust()
teams, err := database.ListTeams(u.ID)
if err != nil {
return err
}
data := struct {
User *User
Teams []*Team
Settings *Settings
}{u, teams, settings}
// if the user hasn't linked their Bitbucket account
// render a different template
if len(u.BitbucketToken) == 0 {
return RenderTemplate(w, "bitbucket_link.html", &data)
}
// otherwise display the template for adding
// a new Bitbucket repository.
return RenderTemplate(w, "bitbucket_add.html", &data)
}
func RepoCreateGithub(w http.ResponseWriter, r *http.Request, u *User) error {
teamName := r.FormValue("team")
owner := r.FormValue("owner")
@ -146,6 +168,84 @@ func RepoCreateGithub(w http.ResponseWriter, r *http.Request, u *User) error {
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
}
func RepoCreateBitbucket(w http.ResponseWriter, r *http.Request, u *User) error {
teamName := r.FormValue("team")
owner := r.FormValue("owner")
name := r.FormValue("name")
// get the bitbucket settings from the database
settings := database.SettingsMust()
// create the Bitbucket client
client := bitbucket.New(
settings.BitbucketKey,
settings.BitbucketSecret,
u.BitbucketToken,
u.BitbucketSecret,
)
bitbucketRepo, err := client.Repos.Find(owner, name)
if err != nil {
return fmt.Errorf("Unable to find Bitbucket repository %s/%s.", owner, name)
}
repo, err := NewBitbucketRepo(owner, name, bitbucketRepo.Private)
if err != nil {
return err
}
repo.UserID = u.ID
repo.Private = bitbucketRepo.Private
// if the user chose to assign to a team account
// we need to retrieve the team, verify the user
// has access, and then set the team id.
if len(teamName) > 0 {
team, err := database.GetTeamSlug(teamName)
if err != nil {
return fmt.Errorf("Unable to find Team %s.", teamName)
}
// user must be an admin member of the team
if ok, _ := database.IsMemberAdmin(u.ID, team.ID); !ok {
return fmt.Errorf("Invalid permission to access Team %s.", teamName)
}
repo.TeamID = team.ID
}
// if the repository is private we'll need
// to upload a bitbucket key to the repository
if repo.Private {
// name the key
keyName := fmt.Sprintf("%s@%s", repo.Owner, settings.Domain)
// create the bitbucket key, or update if one already exists
_, err := client.RepoKeys.CreateUpdate(owner, name, repo.PublicKey, keyName)
if err != nil {
return fmt.Errorf("Unable to add Public Key to your Bitbucket repository: %s", err)
}
} else {
}
// create a hook so that we get notified when code
// is pushed to the repository and can execute a build.
link := fmt.Sprintf("%s://%s/hook/bitbucket.org?id=%s", settings.Scheme, settings.Domain, repo.Slug)
// add the hook
if _, err := client.Brokers.CreateUpdate(owner, name, link, bitbucket.BrokerTypePost); err != nil {
return fmt.Errorf("Unable to add Hook to your Bitbucket repository. %s", err.Error())
}
// Save to the database
if err := database.SaveRepo(repo); err != nil {
return fmt.Errorf("Error saving repository to the database. %s", err)
}
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
}
// Repository Settings
func RepoSettingsForm(w http.ResponseWriter, r *http.Request, u *User, repo *Repo) error {

View file

@ -11,7 +11,7 @@ import (
)
type BuildRunner interface {
Run(buildScript *script.Build, repo *repo.Repo, key []byte, buildOutput io.Writer) (success bool, err error)
Run(buildScript *script.Build, repo *repo.Repo, key []byte, privileged bool, buildOutput io.Writer) (success bool, err error)
}
type buildRunner struct {
@ -26,11 +26,12 @@ func NewBuildRunner(dockerClient *docker.Client, timeout time.Duration) BuildRun
}
}
func (runner *buildRunner) Run(buildScript *script.Build, repo *repo.Repo, key []byte, buildOutput io.Writer) (bool, error) {
func (runner *buildRunner) Run(buildScript *script.Build, repo *repo.Repo, key []byte, privileged bool, buildOutput io.Writer) (bool, error) {
builder := build.New(runner.dockerClient)
builder.Build = buildScript
builder.Repo = repo
builder.Key = key
builder.Privileged = privileged
builder.Stdout = buildOutput
builder.Timeout = runner.timeout

View file

@ -183,6 +183,7 @@ func (w *worker) runBuild(task *BuildTask, buf io.Writer) (bool, error) {
task.Script,
repo,
[]byte(task.Repo.PrivateKey),
task.Repo.Privileged,
buf,
)
}

View file

@ -50,12 +50,12 @@
<input class="form-control form-control-large" type="text" name="GitHubApiUrl" value="{{.Settings.GitHubApiUrl}}" />
</div>
</div>
<div class="form-group hide">
<div class="form-group">
<div class="alert">Bitbucket OAuth Consumer Key and Secret.</div>
<label>Bitbucket Key and Secret:</label>
<div>
<input class="form-control form-control-large" type="text" name="BitbucketKey" value="" />
<input class="form-control form-control-large" type="password" name="BitbucketSecret" value="" />
<input class="form-control form-control-large" type="text" name="BitbucketKey" value="{{.Settings.BitbucketKey}}" />
<input class="form-control form-control-large" type="password" name="BitbucketSecret" value="{{.Settings.BitbucketSecret}}" />
</div>
</div>
<div class="form-group">
@ -119,4 +119,4 @@
return false;
};
</script>
{{ end }}
{{ end }}

View file

@ -0,0 +1,99 @@
{{ define "title" }}Bitbucket · Add Repository{{ end }}
{{ define "content" }}
<div class="subhead">
<div class="container">
<h1>
<span>Repository Setup</span>
<small>Bitbucket</small>
</h1>
</div><!-- ./container -->
</div><!-- ./subhead -->
<div class="container">
<div class="row">
<div class="col-xs-3">
<ul class="nav nav-pills nav-stacked">
<li><a href="/new/github.com">GitHub</a></li>
<li class="active"><a href="/new/bitbucket.org">Bitbucket</a></li>
</ul>
</div><!-- ./col-xs-3 -->
<div class="col-xs-9" role="main">
<div class="alert">
Enter your repository details
<a class="btn btn-default pull-right" href="/auth/login/bitbucket" style="font-size: 18px;background:#f4f4f4;">Re-Link Account</a>
</div>
<form class="form-repo" method="POST" action="/new/bitbucket.org">
<div class="field-group">
<div>
<label>Bitbucket Owner</label>
<div>
<input class="form-control form-control-large" type="text" name="owner" autocomplete="off">
</div>
</div>
</div>
<div class="field-separator">/</div>
<div class="field-group">
<div>
<label>Repository Name</label>
<div>
<input class="form-control form-control-large" type="text" name="name" autocomplete="off">
</div>
</div>
</div>
<br/>
<div class="alert">Select your Drone account</div>
<ul>
<li>
<input type="radio" name="team" checked="True" value="">
<img src="{{ .User.Image }}?s=32">
<span>Me</span>
</li>
{{ range .Teams }}
<li>
<input type="radio" name="team" value="{{ .Slug }}">
<img src="{{ .Image }}?s=32">
<span>{{ .Name }}</span>
</li>
{{ end }}
</ul>
<div class="alert alert-success hide" id="successAlert"></div>
<div class="alert alert-error hide" id="failureAlert"></div>
<div class="form-actions">
<input class="btn btn-primary" id="submitButton" type="submit" value="Add" data-loading-text="Saving ..">
<a class="btn btn-default" href="/dashboard">Cancel</a>
</div>
</form>
</div><!-- ./col-xs-9 -->
</div><!-- ./row -->
</div><!-- ./container -->
{{ end }}
{{ define "script" }}
<script>
document.forms[0].onsubmit = function(event) {
$("#successAlert").hide();
$("#failureAlert").hide();
$('#submitButton').button('loading')
var form = event.target
var formData = new FormData(form);
xhr = new XMLHttpRequest();
xhr.open('POST', form.action);
xhr.onload = function() {
if (this.status == 200) {
var name = $("input[name=name]").val()
var owner = $("input[name=owner]").val()
window.location.pathname = "/bitbucket.org/"+owner+"/"+name
} else {
$("#failureAlert").text("Unable to setup the Repository");
$("#failureAlert").show().removeClass("hide");
$('#submitButton').button('reset')
};
};
xhr.send(formData);
return false;
}
</script>
{{ end }}

View file

@ -0,0 +1,31 @@
{{ define "title" }}Bitbucket · Add Repository{{ end }}
{{ define "content" }}
<div class="subhead">
<div class="container">
<h1>
<span>Repository Setup</span>
<small>Bitbucket</small>
</h1>
</div><!-- ./container -->
</div><!-- ./subhead -->
<div class="container">
<div class="row">
<div class="col-xs-3">
<ul class="nav nav-pills nav-stacked">
<li><a href="/new/github.com">GitHub</a></li>
<li class="active"><a href="/new/bitbucket.org">Bitbucket</a></li>
</ul>
</div><!-- ./col-xs-3 -->
<div class="col-xs-9" role="main">
<div class="alert">Link Your Bitbucket Account
<a class="btn btn-primary pull-right" href="/auth/login/bitbucket" style="font-size: 18px;">Link Now</a>
</div>
</div><!-- ./col-xs-9 -->
</div><!-- ./row -->
</div><!-- ./container -->
{{ end }}
{{ define "script" }}{{ end }}

View file

@ -16,7 +16,7 @@
<ul class="nav nav-pills nav-stacked">
<li class="active"><a href="/new/github.com">GitHub</a></li>
<li><a href="/new/gitlab">GitLab</a></li>
<li><a href="/new/bitbucket.org">Bitbucket <small>(coming soon)</small></a></li>
<li><a href="/new/bitbucket.org">Bitbucket</a></li>
</ul>
</div><!-- ./col-xs-3 -->

View file

@ -16,7 +16,7 @@
<ul class="nav nav-pills nav-stacked">
<li class="active"><a href="/new/github.com">GitHub</a></li>
<li><a href="/new/gitlab">GitLab</a></li>
<li><a href="/new/bitbucket.org">Bitbucket <small>(coming soon)</small></a></li>
<li><a href="/new/bitbucket.org">Bitbucket</a></li>
</ul>
</div><!-- ./col-xs-3 -->

View file

@ -84,6 +84,8 @@ func init() {
"github_link.html",
"gitlab_add.html",
"gitlab_link.html",
"bitbucket_add.html",
"bitbucket_link.html",
}
// extract the base template as a string