diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 5c4176f..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -coverage.db \ No newline at end of file diff --git a/badge.go b/badge.go new file mode 100644 index 0000000..e4ad7e4 --- /dev/null +++ b/badge.go @@ -0,0 +1,74 @@ +package main + +import ( + "bytes" + "fmt" + "net/http" + "text/template" + + "github.com/gorilla/mux" +) + +var baseTemplate *template.Template + +func init() { + baseTemplate = template.New("svgTemplate") + baseTemplate, _ = baseTemplate.Parse(svgTemplate) +} + +func badgeFromCommit(w http.ResponseWriter, r *http.Request) { + commitID := mux.Vars(r)["commit"] + result := svgFromHash(commitID) + + w.Header().Set("Content-Type", "image/svg+xml") + fmt.Fprint(w, result) +} + +func badgeFromProject(w http.ResponseWriter, r *http.Request) { + projectName := mux.Vars(r)["project"] + tagName := mux.Vars(r)["tag"] + commitRow, err := db.Query("SELECT commit_hash FROM alias WHERE project_name=? AND project_tag=?", projectName, tagName) + + if err != nil { + panic(err) + } + if !commitRow.Next() { + fmt.Fprint(w, unknownCoveragePill()) + return + } + + var commitHash string + commitRow.Scan(&commitHash) + commitRow.Close() + + result := svgFromHash(commitHash) + + w.Header().Set("Content-Type", "image/svg+xml") + fmt.Fprint(w, result) +} + +func svgFromHash(commit string) string { + result, err := db.Query("SELECT percentage FROM badge WHERE commit_hash=?", commit) + defer result.Close() + if err != nil { + panic(err) + } + + if !result.Next() { + return unknownCoveragePill() + } + + var percentage float64 + result.Scan(&percentage) + + fillColour := percentageToRGB(percentage) + pill := CoveragePill{ + toOneDecimal(percentage*100) + "%", + baseColour, + fillColour, + } + + buf := &bytes.Buffer{} + baseTemplate.Execute(buf, pill) + return buf.String() +} diff --git a/cmd/server/Dockerfile b/cmd/server/Dockerfile deleted file mode 100644 index 422b10a..0000000 --- a/cmd/server/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM golang:1.16 AS build -WORKDIR /root/ -COPY . . -RUN go build -o coverage . - -FROM golang:1.16 -WORKDIR /root/ -COPY --from=build /root/coverage . -EXPOSE 8080 -CMD ["./coverage"] diff --git a/cmd/server/main.go b/cmd/server/main.go deleted file mode 100644 index 0fadeda..0000000 --- a/cmd/server/main.go +++ /dev/null @@ -1,38 +0,0 @@ -package main - -import ( - "fmt" - "log" - "os" - "os/signal" - - "gitea.teamortix.com/team-ortix/coverage/db" - "gitea.teamortix.com/team-ortix/coverage/server" -) - -var ( - port = os.Getenv("COVERAGE_PORT") - dbFile = os.Getenv("DB_FILE") -) - -func main() { - if port == "" { - port = "8080" - } - if dbFile == "" { - dbFile = "coverage.db" - } - - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt) - - go func() { - db.OpenDatabase(dbFile) - server.StartServer(port) - }() - - log.Printf("server starting. at http://0.0.0.0:%s", port) - - <-c - fmt.Println("\nAborting...") -} diff --git a/connection.go b/connection.go new file mode 100644 index 0000000..fc2c978 --- /dev/null +++ b/connection.go @@ -0,0 +1,22 @@ +package main + +import ( + "database/sql" + _ "github.com/mattn/go-sqlite3" +) + +var db *sql.DB +var alreadyUsingMemory bool + +func init() { + openDatabase("./database.sqlite") +} + +func openDatabase(fileLocation string) { + db, _ = sql.Open("sqlite3", fileLocation) + db.SetMaxOpenConns(1) + + // Initializes the tables + db.Exec(`CREATE TABLE IF NOT EXISTS badge (commit_hash VARCHAR(32) NOT NULL UNIQUE PRIMARY KEY, percentage FLOAT NOT NULL)`) + db.Exec(`CREATE TABLE IF NOT EXISTS alias (commit_hash VARCHAR(32) NOT NULL UNIQUE PRIMARY KEY, project_name TEXT NOT NULL, project_tag TEXT NOT NULL)`) +} diff --git a/database.sqlite b/database.sqlite new file mode 100644 index 0000000..441bb3a Binary files /dev/null and b/database.sqlite differ diff --git a/db/connection.go b/db/connection.go deleted file mode 100644 index 835b046..0000000 --- a/db/connection.go +++ /dev/null @@ -1,33 +0,0 @@ -package db - -import ( - "database/sql" - "log" - - // Required to connect to database. - _ "github.com/mattn/go-sqlite3" -) - -var db *sql.DB - -func OpenDatabase(fileLocation string) { - var err error - db, err = sql.Open("sqlite3", fileLocation) - if err != nil { - log.Fatalf("could not open database: %v", err) - } - - // Initializes the table. - _, err = db.Exec(`CREATE TABLE IF NOT EXISTS badge ( - namespace VARCHAR(64) NOT NULL, - project_name VARCHAR(64) NOT NULL, - coverage INTEGER NOT NULL, - html TEXT NOT NULL, - - branch VARCHAR(64), - pull INTEGER - )`) - if err != nil { - log.Fatalf("Could not create table: %v", err) - } -} diff --git a/db/query.go b/db/query.go deleted file mode 100644 index 4c988d1..0000000 --- a/db/query.go +++ /dev/null @@ -1,75 +0,0 @@ -package db - -import ( - "database/sql" - "errors" - "strconv" -) - -var ErrNoData = errors.New("no badge found with the provided data") - -type CoverageData struct { - Namespace string - Project string - Coverage float64 - HTML string -} - -// QueryByBranch returns CoverageData for the provided branch for the project. -// If there is no such data, it returns ErrNoData. -func QueryByBranch(namespace, project, branch string) (CoverageData, error) { - result, err := db.Query(`SELECT coverage,html FROM badge WHERE - namespace=? AND - project_name=? AND - branch=?`, - namespace, project, branch) - if err != nil { - return CoverageData{}, err - } - - return parseRows(result, namespace, project) -} - -// QueryByBranch returns CoverageData for the provided pull request of the project. -// If there is no such data, it returns ErrNoData. -func QueryByPull(namespace, project, pullStr string) (CoverageData, error) { - pull, err := strconv.Atoi(pullStr) - if err != nil { - return CoverageData{}, ErrNoData - } - - result, err := db.Query(`SELECT coverage, html FROM badge WHERE - namespace=? AND - project_name=? AND - pull=?`, - namespace, project, pull) - if err != nil { - return CoverageData{}, err - } - - return parseRows(result, namespace, project) -} - -func parseRows(result *sql.Rows, namespace, project string) (CoverageData, error) { - if !result.Next() { - return CoverageData{}, ErrNoData - } - - var coverage float64 - var html string - err := result.Scan(&coverage, &html) - if err != nil { - return CoverageData{}, err - } - - if err = result.Close(); err != nil { - return CoverageData{}, ErrNoData - } - - return CoverageData{ - Namespace: namespace, - Project: project, - Coverage: coverage, - HTML: html, - }, nil -} diff --git a/db/upload.go b/db/upload.go deleted file mode 100644 index 73189be..0000000 --- a/db/upload.go +++ /dev/null @@ -1,66 +0,0 @@ -package db - -import ( - "fmt" - "strconv" -) - -func UploadBranchData(namespace, project, branch, coverageStr, html string) error { - coverage, err := strconv.ParseFloat(coverageStr, 64) - if err != nil { - return fmt.Errorf("coverage param must be valid float, instead got %s", coverageStr) - } - - // Because we don't have have a primary key, we must do the upsert manually. - _, err = QueryByBranch(namespace, project, branch) - if err == ErrNoData { - _, err = db.Exec("INSERT INTO badge (namespace, project_name, branch, coverage, html) VALUES (?, ?, ?, ?, ?)", - namespace, project, branch, coverage, html) - if err != nil { - return fmt.Errorf("could not update database: %v", err) - } - } - if err != nil { - return err - } - - _, err = db.Exec("UPDATE badge SET coverage=?, html=? WHERE namespace=? AND project_name=? AND branch=?", - coverage, html, namespace, project, branch) - if err != nil { - return fmt.Errorf("could not update database: %v", err) - } - - return nil -} - -func UploadPullData(namespace, project, pullStr, coverageStr, html string) error { - pull, err := strconv.Atoi(pullStr) - if err != nil { - return fmt.Errorf("pull param must be valid int, instead got %s", pullStr) - } - coverage, err := strconv.ParseFloat(coverageStr, 64) - if err != nil { - return fmt.Errorf("coverage param must be valid float, instead got %s", coverageStr) - } - - // Because we don't have have a primary key, we must do the upsert manually. - _, err = QueryByPull(namespace, project, pullStr) - if err == ErrNoData { - _, err = db.Exec("INSERT INTO badge (namespace, project_name, pull, coverage, html) VALUES (?, ?, ?, ?, ?)", - namespace, project, pull, coverage, html) - if err != nil { - return fmt.Errorf("could not update database: %v", err) - } - } - if err != nil { - return err - } - - _, err = db.Exec("UPDATE badge SET coverage=?, html=? WHERE namespace=? AND project_name=? AND pull=?", - coverage, html, namespace, project, pull) - if err != nil { - return fmt.Errorf("could not update database: %v", err) - } - - return nil -} diff --git a/go.mod b/go.mod deleted file mode 100644 index 573805c..0000000 --- a/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module gitea.teamortix.com/team-ortix/coverage - -go 1.16 - -require ( - github.com/gorilla/mux v1.8.0 - github.com/mattn/go-sqlite3 v1.14.6 -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 27294bc..0000000 --- a/go.sum +++ /dev/null @@ -1,4 +0,0 @@ -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= -github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= diff --git a/main.go b/main.go new file mode 100644 index 0000000..7ed99af --- /dev/null +++ b/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/gorilla/mux" +) + +func main() { + r := mux.NewRouter() + r.HandleFunc("/upload/go", uploadGo) + r.HandleFunc("/badge/commit/{commit}.svg", badgeFromCommit) + r.HandleFunc("/badge/{project}/{tag}.svg", badgeFromProject) + _ = http.ListenAndServe(":8080", r) +} + +func parseCoverage(content string) float64 { + var totalStatements = 0 + var totalRun = 0 + + lines := strings.Split(content, "\n") + for _, line := range lines[1 : len(lines)-1] { // Skip first line (which is not relevant) + sl := strings.Split(line, " ") + statements, _ := strconv.Atoi(sl[len(sl)-2]) + runCount, _ := strconv.Atoi(sl[len(sl)-1]) + + totalStatements += statements + if runCount != 0 { + totalRun += statements + } + } + fmt.Printf("DEBUG :: TotalStatements: %d, TotalRunCount: %d\n", totalStatements, totalRun) + return float64(totalRun) / float64(totalStatements) +} diff --git a/pill.go b/pill.go new file mode 100644 index 0000000..7ff2705 --- /dev/null +++ b/pill.go @@ -0,0 +1,85 @@ +package main + +import ( + "bytes" + "fmt" + "math" + "strconv" +) + +const svgTemplate string = ` + + + + + + + + + + + + + + coverage + coverage + {{.Percentage}} + {{.Percentage}} + +` + +const ( + baseColour = "#555" + modifier float64 = 0.2 + maxValue = 220 + blue = 55 +) + +//Pill used for template +type CoveragePill struct { + Percentage string + BaseColour string + FillColour string +} + +func unknownCoveragePill() string { + pill := CoveragePill{ + "?", + baseColour, + baseColour, // Using the base colour for fill colour because unknown + } + + buf := &bytes.Buffer{} + baseTemplate.Execute(buf, pill) + return buf.String() +} + +func percentageToRGB(percentage float64) string { + red := modifier + clamp(2-2*math.Pow(percentage, 2), 0, 1)*(1-modifier) + green := modifier + clamp(2*math.Pow(percentage, 2), 0, 1)*(1-modifier) + + redValue := strconv.Itoa(int(red * maxValue)) + greenValue := strconv.Itoa(int(green * maxValue)) + blueValue := strconv.Itoa(blue) + + return "rgb(" + redValue + ", " + greenValue + ", " + blueValue + ")" +} + +func clamp(operation float64, min float64, max float64) float64 { + if operation < min { + return min + } + if operation > max { + return max + } + return operation +} + +func round(num float64) int { + return int(num + math.Copysign(0.5, num)) +} + +func toOneDecimal(num float64) string { + output := math.Pow(10, float64(5)) + return fmt.Sprintf("%.1f", float64(round(num*output))/output) +} diff --git a/pill/badge.go b/pill/badge.go deleted file mode 100644 index bfcec92..0000000 --- a/pill/badge.go +++ /dev/null @@ -1 +0,0 @@ -package pill diff --git a/pill/pill.go b/pill/pill.go deleted file mode 100644 index 5edf9db..0000000 --- a/pill/pill.go +++ /dev/null @@ -1,79 +0,0 @@ -package pill - -import ( - // Used for embedding pill. - _ "embed" - "fmt" - "io" - "math" - "text/template" -) - -var ( - //go:embed pill.svg - pill string - - tmpl = template.New("svgTemplate") -) - -const ( - modifier = 0.2 - max = 190 - blue = 55 -) - -// Pill is used to execute the pill.svg template. -// Width is dependent on whether the percentage is 2 digits or 3 digits. -type Pill struct { - Percentage string - Colour string - Width string -} - -// NewPill creates creates a Pill, automatically calculating the colour, width, and formatting the percentage. -// Note: %.0f gives us a float with 0 decimals and 0 padding for 2 numbers less than 10. -func NewPill(percentDecimal float64) Pill { - square := math.Pow(percentDecimal, 2) - red := modifier + clamp(2-2*square, 0, 1)*(1-modifier) - green := modifier + clamp(2*square, 0, 1)*(1-modifier) - - percentage := fmt.Sprintf("%.0f", percentDecimal*100) - colour := fmt.Sprintf("rgb(%.0f, %.0f, %d)", red*max, green*max, blue) - width := "250" - if len(percentage) == 1 { - percentage = " " + percentage - width = "220" - } - if len(percentage) == 3 { - width = "300" - } - - return Pill{ - Percentage: percentage, - Colour: colour, - Width: width, - } -} - -// Execute runs the pill template with the data and writes the result to the writer given. -func (p Pill) Execute(w io.Writer) error { - var err error - tmpl, err = tmpl.Parse(pill) - if err != nil { - return err - } - - err = tmpl.Execute(w, p) - return err -} - -func clamp(n, min, max float64) float64 { - switch { - case n > max: - return max - case n < min: - return min - default: - return n - } -} diff --git a/pill/pill.svg b/pill/pill.svg deleted file mode 100644 index ff9012c..0000000 --- a/pill/pill.svg +++ /dev/null @@ -1,11 +0,0 @@ - - coverage: {{.Percentage}}% - - - - - - coverage - {{.Percentage}}% - - \ No newline at end of file diff --git a/server/query.go b/server/query.go deleted file mode 100644 index a73d6a0..0000000 --- a/server/query.go +++ /dev/null @@ -1,92 +0,0 @@ -package server - -import ( - "fmt" - "log" - "net/http" - - "gitea.teamortix.com/team-ortix/coverage/db" - "gitea.teamortix.com/team-ortix/coverage/pill" -) - -type endpointData struct { - thirdParam string - queryFunc func(string, string, string) (db.CoverageData, error) - handle func(db.CoverageData) -} - -func (d endpointData) run(w http.ResponseWriter, r *http.Request) { - params, ok := getAllParams(w, r, d.thirdParam) - if !ok { - return - } - - data, err := d.queryFunc(params[0], params[1], params[2]) - if err == db.ErrNoData { - http.Error(w, "not found", http.StatusNotFound) - return - } - if err != nil { - http.Error(w, fmt.Sprintf("could not query from db: %v", err), http.StatusInternalServerError) - log.Printf("error occurred while querying data from db: %v", err) - return - } - - d.handle(data) -} - -func handleBranchReport(w http.ResponseWriter, r *http.Request) { - endpointData{ - thirdParam: "branch", - queryFunc: db.QueryByBranch, - handle: func(cd db.CoverageData) { - w.Header().Set("Content-Type", "text/html") - fmt.Fprint(w, cd.HTML) - }, - }.run(w, r) -} - -func handleBadgeBranch(w http.ResponseWriter, r *http.Request) { - endpointData{ - thirdParam: "branch", - queryFunc: db.QueryByBranch, - handle: func(cd db.CoverageData) { - p := pill.NewPill(cd.Coverage) - - w.Header().Set("Content-Type", "image/svg+xml") - err := p.Execute(w) - if err != nil { - http.Error(w, fmt.Sprintf("could not write pill: %v", err), http.StatusInternalServerError) - return - } - }, - }.run(w, r) -} - -func handlePullReport(w http.ResponseWriter, r *http.Request) { - endpointData{ - thirdParam: "pull", - queryFunc: db.QueryByPull, - handle: func(cd db.CoverageData) { - w.Header().Set("Content-Type", "text/html") - fmt.Fprint(w, cd.HTML) - }, - }.run(w, r) -} - -func handlePullBadge(w http.ResponseWriter, r *http.Request) { - endpointData{ - thirdParam: "pull", - queryFunc: db.QueryByPull, - handle: func(cd db.CoverageData) { - p := pill.NewPill(cd.Coverage) - - w.Header().Set("Content-Type", "image/svg+xml") - err := p.Execute(w) - if err != nil { - http.Error(w, fmt.Sprintf("could not write pill: %v", err), http.StatusInternalServerError) - return - } - }, - }.run(w, r) -} diff --git a/server/server.go b/server/server.go deleted file mode 100644 index 2fe3953..0000000 --- a/server/server.go +++ /dev/null @@ -1,76 +0,0 @@ -package server - -import ( - "fmt" - "log" - "net/http" - "strconv" - - "gitea.teamortix.com/team-ortix/coverage/pill" - "github.com/gorilla/mux" -) - -func StartServer(port string) { - r := mux.NewRouter() - r.HandleFunc("/badge/{percentage}", handleBadge) - - r.HandleFunc("/report/branch/{namespace}/{project}/{branch}", handleBranchReport) - r.HandleFunc("/report/pulls/{namespace}/{project}/{pull}", handlePullReport) - r.HandleFunc("/badge/branch/{namespace}/{project}/{branch}", handleBadgeBranch) - r.HandleFunc("/badge/pulls/{namespace}/{project}/{pull}", handlePullBadge) - - r.HandleFunc("/upload/branch/{namespace}/{project}/{branch}", uploadBranch) - r.HandleFunc("/upload/pulls/{namespace}/{project}/{pull}", uploadPull) - - portStr := fmt.Sprintf(":%s", port) - err := http.ListenAndServe(portStr, r) - if err != nil { - log.Fatalf("could not start server: %v", err) - } -} - -func handleBadge(w http.ResponseWriter, r *http.Request) { - percentString, ok := mux.Vars(r)["percentage"] - if !ok { - http.Error(w, "could not find percentage in path", http.StatusBadRequest) - return - } - - percent, err := strconv.ParseFloat(percentString, 64) - if err != nil { - http.Error(w, fmt.Sprintf("could not convert %s to a number", percentString), http.StatusBadRequest) - return - } - - p := pill.NewPill(percent / 100) - - w.Header().Set("Content-Type", "image/svg+xml") - err = p.Execute(w) - if err != nil { - http.Error(w, fmt.Sprintf("could not write pill: %v", err), http.StatusInternalServerError) - return - } -} - -func getAllParams(w http.ResponseWriter, r *http.Request, thirdParam string) ([3]string, bool) { - vars := mux.Vars(r) - namespace, ok := vars["namespace"] - if !ok { - http.Error(w, "Path 'namespace' not found", http.StatusBadRequest) - return [3]string{}, false - } - - project, ok := vars["project"] - if !ok { - http.Error(w, "Path 'project' not found", http.StatusBadRequest) - return [3]string{}, false - } - - third, ok := vars[thirdParam] - if !ok { - http.Error(w, fmt.Sprintf("Path '%s' not found", thirdParam), http.StatusBadRequest) - return [3]string{}, false - } - - return [3]string{namespace, project, third}, true -} diff --git a/server/upload.go b/server/upload.go deleted file mode 100644 index 98c2ec5..0000000 --- a/server/upload.go +++ /dev/null @@ -1,76 +0,0 @@ -package server - -import ( - "fmt" - "io" - "net/http" - "os" - - "gitea.teamortix.com/team-ortix/coverage/db" -) - -const coverageSecret = "COVERAGE_SECRET" - -var secretEnv = os.Getenv(coverageSecret) - -func uploadBranch(w http.ResponseWriter, r *http.Request) { - upload(w, r, "branch", db.UploadBranchData) -} - -func uploadPull(w http.ResponseWriter, r *http.Request) { - upload(w, r, "pull", db.UploadPullData) -} - -func upload(w http.ResponseWriter, r *http.Request, - uploadType string, uploadFunc func(string, string, string, string, string) error) { - if r.Method != "POST" { - return - } - - err := r.ParseMultipartForm(5 << 20) - if err != nil { - http.Error(w, "file too large", http.StatusRequestEntityTooLarge) - return - } - err = r.ParseForm() - if err != nil { - http.Error(w, "could not parse form", http.StatusBadRequest) - return - } - - secret := r.Form.Get("secret") - params, ok := getAllParams(w, r, uploadType) - coverage := r.Form.Get("coverage") - - if secretEnv != secret && secretEnv != "" { - http.Error(w, "invalid secret provided", http.StatusUnauthorized) - return - } - if !ok { - return - } - - if coverage == "" { - http.Error(w, "request param coverage must be present", http.StatusBadRequest) - return - } - - report, _, err := r.FormFile("report") - if err != nil { - http.Error(w, "could not parse report parameter", http.StatusBadRequest) - return - } - bytes, err := io.ReadAll(report) - if err != nil { - http.Error(w, fmt.Sprintf("could not read file: %v", err), http.StatusBadRequest) - return - } - - err = uploadFunc(params[0], params[1], params[2], coverage, string(bytes)) - if err != nil { - http.Error(w, fmt.Sprintf("could not upload data to database: %v", err), http.StatusInternalServerError) - return - } - - fmt.Fprintln(w, "uploaded") -} diff --git a/test.svg b/test.svg new file mode 100644 index 0000000..b84c231 --- /dev/null +++ b/test.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + coverage + coverage + 99.9% + 99.9% + + diff --git a/upload.go b/upload.go new file mode 100644 index 0000000..418c3e9 --- /dev/null +++ b/upload.go @@ -0,0 +1,56 @@ +package main + +import ( + "database/sql" + "fmt" + "io/ioutil" + "net/http" +) + +func uploadGo(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + return + } + w.Header().Set("Content-Type", "application/json") + + err := r.ParseForm() + if err != nil { + http.Error(w, `{"success": false, "message": "Request parameters not parsed"}`, 500) + return + } + err = r.ParseMultipartForm(5 << 20) + if err != nil { + http.Error(w, `{"success": false, "message": "Request parameters too large"}`, 500) + return + } + + name := r.Form.Get("project-name") + tag := r.Form.Get("tag") + id := r.Form.Get("id") + + if name == "" || tag == "" || id == "" { + http.Error(w, `{"success": false, "message": "Request parameters [secret, name, tag, id] not found"}`, 400) + return + } + file, _, err := r.FormFile("file") + if err != nil { + http.Error(w, `{"success": false, "message": "Request parameter file not found"}`, 400) + return + } + b, _ := ioutil.ReadAll(file) + coverage := parseCoverage(string(b)) + + db.Exec("INSERT INTO badge (commit_hash, percentage) VALUES (?, ?)", id, coverage) + + _, err = db.Query("SELECT commit_hash FROM alias WHERE project_name=? AND project_tag=?", name, tag) + + if err == nil { + db.Exec("UPDATE ALIAS SET commit_hash=? WHERE project_name=? AND project_tag=?", id, name, tag) + } else if err == sql.ErrNoRows { + db.Exec("INSERT INTO alias (commit_hash, project_name, project_tag VALUES (?, ?, ?)", id, name, tag) + } else { + panic(err) + } + + fmt.Fprintf(w, `{"success": "true", "coverage": %s%%}`, toOneDecimal(coverage*100)) +}