1
0
Fork 0
mirror of https://github.com/Luzifer/clean-github-branches.git synced 2024-11-09 14:40:04 +00:00

Log branch age

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2018-09-07 12:05:52 +02:00
parent 2b085e2e87
commit dd8da5cac4
Signed by: luzifer
GPG key ID: DC2729FDD34BE99E
7 changed files with 583 additions and 2 deletions

17
Gopkg.lock generated
View file

@ -1,6 +1,14 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:52b6c8beb19a31fe7b40b23c9b694caf4213cfa3d6cce711cd0636db68ce67aa"
name = "github.com/Luzifer/go_helpers"
packages = ["duration"]
pruneopts = "NUT"
revision = "4e368ddb27c0a08a0c84e5ceac52d57b464bffce"
version = "v2.7.0"
[[projects]] [[projects]]
digest = "1:3c2b6707179ff866ca06c5eecd544c9079a6ec35d735031a1bf61abfff336186" digest = "1:3c2b6707179ff866ca06c5eecd544c9079a6ec35d735031a1bf61abfff336186"
name = "github.com/Luzifer/rconfig" name = "github.com/Luzifer/rconfig"
@ -33,6 +41,14 @@
pruneopts = "NUT" pruneopts = "NUT"
revision = "53e6ce116135b80d037921a7fdd5138cf32d7a8a" revision = "53e6ce116135b80d037921a7fdd5138cf32d7a8a"
[[projects]]
branch = "master"
digest = "1:071a57a67bde66b0e6438ffee4599ef99532a0e29de3ea12926b5438608f90a6"
name = "github.com/leekchan/gtf"
packages = ["."]
pruneopts = "NUT"
revision = "79e3a68ab435edb72c6490289a33ef8bedcee15a"
[[projects]] [[projects]]
digest = "1:5cf3f025cbee5951a4ee961de067c8a89fc95a5adabead774f82822efabab121" digest = "1:5cf3f025cbee5951a4ee961de067c8a89fc95a5adabead774f82822efabab121"
name = "github.com/pkg/errors" name = "github.com/pkg/errors"
@ -134,6 +150,7 @@
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
input-imports = [ input-imports = [
"github.com/Luzifer/go_helpers/duration",
"github.com/Luzifer/rconfig", "github.com/Luzifer/rconfig",
"github.com/google/go-github/github", "github.com/google/go-github/github",
"github.com/pkg/errors", "github.com/pkg/errors",

View file

@ -25,6 +25,10 @@
# unused-packages = true # unused-packages = true
[[constraint]]
name = "github.com/Luzifer/go_helpers"
version = "2.7.0"
[[constraint]] [[constraint]]
name = "github.com/Luzifer/rconfig" name = "github.com/Luzifer/rconfig"
version = "2.1.0" version = "2.1.0"

View file

@ -28,8 +28,8 @@ Usage of clean-github-branches:
--version Prints current version and exits --version Prints current version and exits
$ clean-github-branches -r '^[lL]uzifer(-docker|-ansible|)/' $ clean-github-branches -r '^[lL]uzifer(-docker|-ansible|)/'
WARN[0013] Stale branch found ahead=1 behind=1 branch=develop dry-run=true repo=luzifer-docker/etherpad-lite WARN[0012] Stale branch found age=2y174d23h ahead=1 behind=1 branch=develop dry-run=true repo=luzifer-docker/etherpad-lite
INFO[0013] Done. INFO[0012] Done.
``` ```
All parameters causing destructive actions are set to sane defaults: By default a `dry-run` is done which prevents any deletion. Also `delete-stale` is disabled as it might cause data loss as the branch is not merged and all commits in it will be lost. All parameters causing destructive actions are set to sane defaults: By default a `dry-run` is done which prevents any deletion. Also `delete-stale` is disabled as it might cause data loss as the branch is not merged and all commits in it will be lost.

View file

@ -10,6 +10,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/Luzifer/go_helpers/duration"
"github.com/Luzifer/rconfig" "github.com/Luzifer/rconfig"
"github.com/google/go-github/github" "github.com/google/go-github/github"
@ -18,6 +19,10 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
const humanizeTemplate = `{{if gt .Years 0}}{{.Years}}y{{end}}` +
`{{if gt .Days 0}}{{.Days}}d{{end}}` +
`{{if gt .Hours 0}}{{.Hours}}h{{end}}`
var ( var (
cfg = struct { cfg = struct {
BranchStaleness time.Duration `flag:"branch-staleness" default:"2160h" description:"When to see a branch as stale (default 90d)"` BranchStaleness time.Duration `flag:"branch-staleness" default:"2160h" description:"When to see a branch as stale (default 90d)"`
@ -277,6 +282,8 @@ func processRepo(ctx context.Context, logger *log.Entry, client *github.Client,
float64(time.Since(b.GetCommit().GetCommit().GetAuthor().GetDate())), float64(time.Since(b.GetCommit().GetCommit().GetAuthor().GetDate())),
float64(time.Since(b.GetCommit().GetCommit().GetCommitter().GetDate())), float64(time.Since(b.GetCommit().GetCommit().GetCommitter().GetDate())),
)) ))
d, _ := duration.CustomHumanizeDuration(branchLastModified, humanizeTemplate)
branchLogger = branchLogger.WithField("age", d)
// Check all PRs whether they match the branch (head) and are merged // Check all PRs whether they match the branch (head) and are merged
hasValidMerge, hasOpenPR := analysePullRequests(branchLogger, prs, b) hasValidMerge, hasOpenPR := analysePullRequests(branchLogger, prs, b)

61
vendor/github.com/Luzifer/go_helpers/duration/time.go generated vendored Normal file
View file

@ -0,0 +1,61 @@
package duration
import (
"bytes"
"math"
"strings"
"text/template"
"time"
"github.com/leekchan/gtf"
)
const defaultDurationFormat = `{{if gt .Years 0}}{{.Years}} year{{.Years|pluralize "s"}}, {{end}}` +
`{{if gt .Days 0}}{{.Days}} day{{.Days|pluralize "s"}}, {{end}}` +
`{{if gt .Hours 0}}{{.Hours}} hour{{.Hours|pluralize "s"}}, {{end}}` +
`{{if gt .Minutes 0}}{{.Minutes}} minute{{.Minutes|pluralize "s"}}, {{end}}` +
`{{if gt .Seconds 0}}{{.Seconds}} second{{.Seconds|pluralize "s"}}{{end}}`
func HumanizeDuration(in time.Duration) string {
f, err := CustomHumanizeDuration(in, defaultDurationFormat)
if err != nil {
panic(err)
}
return strings.Trim(f, " ,")
}
func CustomHumanizeDuration(in time.Duration, tpl string) (string, error) {
result := struct{ Years, Days, Hours, Minutes, Seconds int64 }{}
in = time.Duration(math.Abs(float64(in)))
for in > 0 {
switch {
case in > 365.25*24*time.Hour:
result.Years = int64(in / (365 * 24 * time.Hour))
in = in - time.Duration(result.Years)*365*24*time.Hour
case in > 24*time.Hour:
result.Days = int64(in / (24 * time.Hour))
in = in - time.Duration(result.Days)*24*time.Hour
case in > time.Hour:
result.Hours = int64(in / time.Hour)
in = in - time.Duration(result.Hours)*time.Hour
case in > time.Minute:
result.Minutes = int64(in / time.Minute)
in = in - time.Duration(result.Minutes)*time.Minute
default:
result.Seconds = int64(in / time.Second)
in = 0
}
}
tmpl, err := template.New("timeformat").Funcs(template.FuncMap(gtf.GtfFuncMap)).Parse(tpl)
if err != nil {
return "", err
}
buf := bytes.NewBuffer([]byte{})
tmpl.Execute(buf, result)
return buf.String(), nil
}

22
vendor/github.com/leekchan/gtf/LICENSE generated vendored Normal file
View file

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 Kyoung-chan Lee (leekchan@gmail.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

470
vendor/github.com/leekchan/gtf/gtf.go generated vendored Normal file
View file

@ -0,0 +1,470 @@
package gtf
import (
"fmt"
htmlTemplate "html/template"
"math"
"math/rand"
"net/url"
"reflect"
"regexp"
"strings"
textTemplate "text/template"
"time"
)
var striptagsRegexp = regexp.MustCompile("<[^>]*?>")
// recovery will silently swallow all unexpected panics.
func recovery() {
recover()
}
var GtfTextFuncMap = textTemplate.FuncMap{
"replace": func(s1 string, s2 string) string {
defer recovery()
return strings.Replace(s2, s1, "", -1)
},
"title": func(s string) string {
defer recovery()
return strings.Title(s)
},
"default": func(arg interface{}, value interface{}) interface{} {
defer recovery()
v := reflect.ValueOf(value)
switch v.Kind() {
case reflect.String, reflect.Slice, reflect.Array, reflect.Map:
if v.Len() == 0 {
return arg
}
case reflect.Bool:
if !v.Bool() {
return arg
}
default:
return value
}
return value
},
"length": func(value interface{}) int {
defer recovery()
v := reflect.ValueOf(value)
switch v.Kind() {
case reflect.Slice, reflect.Array, reflect.Map:
return v.Len()
case reflect.String:
return len([]rune(v.String()))
}
return 0
},
"lower": func(s string) string {
defer recovery()
return strings.ToLower(s)
},
"upper": func(s string) string {
defer recovery()
return strings.ToUpper(s)
},
"truncatechars": func(n int, s string) string {
defer recovery()
if n < 0 {
return s
}
r := []rune(s)
rLength := len(r)
if n >= rLength {
return s
}
if n > 3 && rLength > 3 {
return string(r[:n-3]) + "..."
}
return string(r[:n])
},
"urlencode": func(s string) string {
defer recovery()
return url.QueryEscape(s)
},
"wordcount": func(s string) int {
defer recovery()
return len(strings.Fields(s))
},
"divisibleby": func(arg interface{}, value interface{}) bool {
defer recovery()
var v float64
switch value.(type) {
case int, int8, int16, int32, int64:
v = float64(reflect.ValueOf(value).Int())
case uint, uint8, uint16, uint32, uint64:
v = float64(reflect.ValueOf(value).Uint())
case float32, float64:
v = reflect.ValueOf(value).Float()
default:
return false
}
var a float64
switch arg.(type) {
case int, int8, int16, int32, int64:
a = float64(reflect.ValueOf(arg).Int())
case uint, uint8, uint16, uint32, uint64:
a = float64(reflect.ValueOf(arg).Uint())
case float32, float64:
a = reflect.ValueOf(arg).Float()
default:
return false
}
return math.Mod(v, a) == 0
},
"lengthis": func(arg int, value interface{}) bool {
defer recovery()
v := reflect.ValueOf(value)
switch v.Kind() {
case reflect.Slice, reflect.Array, reflect.Map:
return v.Len() == arg
case reflect.String:
return len([]rune(v.String())) == arg
}
return false
},
"trim": func(s string) string {
defer recovery()
return strings.TrimSpace(s)
},
"capfirst": func(s string) string {
defer recovery()
return strings.ToUpper(string(s[0])) + s[1:]
},
"pluralize": func(arg string, value interface{}) string {
defer recovery()
flag := false
v := reflect.ValueOf(value)
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
flag = v.Int() == 1
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
flag = v.Uint() == 1
default:
return ""
}
if !strings.Contains(arg, ",") {
arg = "," + arg
}
bits := strings.Split(arg, ",")
if len(bits) > 2 {
return ""
}
if flag {
return bits[0]
}
return bits[1]
},
"yesno": func(yes string, no string, value bool) string {
defer recovery()
if value {
return yes
}
return no
},
"rjust": func(arg int, value string) string {
defer recovery()
n := arg - len([]rune(value))
if n > 0 {
value = strings.Repeat(" ", n) + value
}
return value
},
"ljust": func(arg int, value string) string {
defer recovery()
n := arg - len([]rune(value))
if n > 0 {
value = value + strings.Repeat(" ", n)
}
return value
},
"center": func(arg int, value string) string {
defer recovery()
n := arg - len([]rune(value))
if n > 0 {
left := n / 2
right := n - left
value = strings.Repeat(" ", left) + value + strings.Repeat(" ", right)
}
return value
},
"filesizeformat": func(value interface{}) string {
defer recovery()
var size float64
v := reflect.ValueOf(value)
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
size = float64(v.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
size = float64(v.Uint())
case reflect.Float32, reflect.Float64:
size = v.Float()
default:
return ""
}
var KB float64 = 1 << 10
var MB float64 = 1 << 20
var GB float64 = 1 << 30
var TB float64 = 1 << 40
var PB float64 = 1 << 50
filesizeFormat := func(filesize float64, suffix string) string {
return strings.Replace(fmt.Sprintf("%.1f %s", filesize, suffix), ".0", "", -1)
}
var result string
if size < KB {
result = filesizeFormat(size, "bytes")
} else if size < MB {
result = filesizeFormat(size/KB, "KB")
} else if size < GB {
result = filesizeFormat(size/MB, "MB")
} else if size < TB {
result = filesizeFormat(size/GB, "GB")
} else if size < PB {
result = filesizeFormat(size/TB, "TB")
} else {
result = filesizeFormat(size/PB, "PB")
}
return result
},
"apnumber": func(value interface{}) interface{} {
defer recovery()
name := [10]string{"one", "two", "three", "four", "five",
"six", "seven", "eight", "nine"}
v := reflect.ValueOf(value)
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if v.Int() < 10 {
return name[v.Int()-1]
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if v.Uint() < 10 {
return name[v.Uint()-1]
}
}
return value
},
"intcomma": func(value interface{}) string {
defer recovery()
v := reflect.ValueOf(value)
var x uint
minus := false
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if v.Int() < 0 {
minus = true
x = uint(-v.Int())
} else {
x = uint(v.Int())
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
x = uint(v.Uint())
default:
return ""
}
var result string
for x >= 1000 {
result = fmt.Sprintf(",%03d%s", x%1000, result)
x /= 1000
}
result = fmt.Sprintf("%d%s", x, result)
if minus {
result = "-" + result
}
return result
},
"ordinal": func(value interface{}) string {
defer recovery()
v := reflect.ValueOf(value)
var x uint
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if v.Int() < 0 {
return ""
}
x = uint(v.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
x = uint(v.Uint())
default:
return ""
}
suffixes := [10]string{"th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th"}
switch x % 100 {
case 11, 12, 13:
return fmt.Sprintf("%d%s", x, suffixes[0])
}
return fmt.Sprintf("%d%s", x, suffixes[x%10])
},
"first": func(value interface{}) interface{} {
defer recovery()
v := reflect.ValueOf(value)
switch v.Kind() {
case reflect.String:
return string([]rune(v.String())[0])
case reflect.Slice, reflect.Array:
return v.Index(0).Interface()
}
return ""
},
"last": func(value interface{}) interface{} {
defer recovery()
v := reflect.ValueOf(value)
switch v.Kind() {
case reflect.String:
str := []rune(v.String())
return string(str[len(str)-1])
case reflect.Slice, reflect.Array:
return v.Index(v.Len() - 1).Interface()
}
return ""
},
"join": func(arg string, value []string) string {
defer recovery()
return strings.Join(value, arg)
},
"slice": func(start int, end int, value interface{}) interface{} {
defer recovery()
v := reflect.ValueOf(value)
if start < 0 {
start = 0
}
switch v.Kind() {
case reflect.String:
str := []rune(v.String())
if end > len(str) {
end = len(str)
}
return string(str[start:end])
case reflect.Slice:
return v.Slice(start, end).Interface()
}
return ""
},
"random": func(value interface{}) interface{} {
defer recovery()
rand.Seed(time.Now().UTC().UnixNano())
v := reflect.ValueOf(value)
switch v.Kind() {
case reflect.String:
str := []rune(v.String())
return string(str[rand.Intn(len(str))])
case reflect.Slice, reflect.Array:
return v.Index(rand.Intn(v.Len())).Interface()
}
return ""
},
"striptags": func(s string) string {
return strings.TrimSpace(striptagsRegexp.ReplaceAllString(s, ""))
},
}
var GtfFuncMap = htmlTemplate.FuncMap(GtfTextFuncMap)
// gtf.New is a wrapper function of template.New(https://golang.org/pkg/html/template/#New).
// It automatically adds the gtf functions to the template's function map
// and returns template.Template(http://golang.org/pkg/html/template/#Template).
func New(name string) *htmlTemplate.Template {
return htmlTemplate.New(name).Funcs(GtfFuncMap)
}
// gtf.Inject injects gtf functions into the passed FuncMap.
// It does not overwrite the original function which have same name as a gtf function.
func Inject(funcs map[string]interface{}) {
for k, v := range GtfFuncMap {
if _, ok := funcs[k]; !ok {
funcs[k] = v
}
}
}
// gtf.ForceInject injects gtf functions into the passed FuncMap.
// It overwrites the original function which have same name as a gtf function.
func ForceInject(funcs map[string]interface{}) {
for k, v := range GtfFuncMap {
funcs[k] = v
}
}
// gtf.Inject injects gtf functions into the passed FuncMap.
// It prefixes the gtf functions with the specified prefix.
// If there are many function which have same names as the gtf functions,
// you can use this function to prefix the gtf functions.
func InjectWithPrefix(funcs map[string]interface{}, prefix string) {
for k, v := range GtfFuncMap {
funcs[prefix+k] = v
}
}