package repostatus import ( "errors" "path/filepath" "regexp" "slices" "strings" "sync" "time" "go.wit.com/gui" "go.wit.com/lib/gui/shell" "go.wit.com/log" ) type Tag struct { // tracks if the tag is displayed hidden bool // the tag "v0.1.3" tag *gui.Node // the .git/ref hash ref *gui.Node // the .git/ref date date *gui.Node duration *gui.Node // the tag comment subject *gui.Node // a button to delete the tag deleteB *gui.Node } // a GUI box of all the tags in a repo type GitTagBox struct { // the box to list all the tags in box *gui.Node group *gui.Node grid *gui.Node // all the tags tags []*Tag } func (rs *RepoStatus) makeTagBox(box *gui.Node) error { if rs.Tags != nil { log.Log(WARN, "already scanned tags") return errors.New("already scanned tags") } tagB := new(GitTagBox) rs.Tags = tagB tagB.group = box.NewGroup(".git tags for " + rs.String()) // tagB.group.NewButton("prune tags", func() { // tagB.Prune() // }) var dups *gui.Node dups = tagB.group.NewCheckbox("Show duplicate tags").SetChecked(false) dups.Custom = func() { if dups.Checked() { tagB.Prune() } else { for _, t := range tagB.tags { t.Show() } } } tagB.group.NewButton("delete all", func() { for i, t := range tagB.tags { if t.hidden { // log.Info("tag is hidden", i, t.tag.String()) continue } log.Info("tag is shown", i, t.tag.String()) rs.DeleteTag(t) } }) grid := tagB.group.NewGrid("tags", 0, 0) tagB.grid = grid grid.NewLabel("version") grid.NewLabel("ref") grid.NewLabel("date") grid.NewLabel("release subject") // works like a typerwriter grid.NextRow() // git tag --list --sort=taggerdate // git for-each-ref --sort=taggerdate --format '%(tag) %(*objectname) %(taggerdate)' // git rev-parse HEAD // if last tag == HEAD, then remove it tags := []string{"%(objectname)", "%(creatordate)", "%(*authordate)", "%(refname)", "%(subject)"} format := strings.Join(tags, "_,,,_") cmd := []string{"git", "for-each-ref", "--sort=taggerdate", "--format", format} // log.Info("RUNNING:", strings.Join(cmd, " ")) r := shell.PathRunQuiet(rs.Path(), cmd) if r.Error != nil { log.Warn("git for-each-ref error:", r.Error) return r.Error } lines := r.Stdout // reverse the git order slices.Reverse(lines) tagB.tags = make([]*Tag, 0) for i, line := range lines { var parts []string parts = make([]string, 0) parts = strings.Split(line, "_,,,_") if len(parts) != 5 { log.Info("tag error:", i, parts) continue } // log.Info("found tag:", i, parts) rTag := new(Tag) rTag.tag = grid.NewLabel(parts[3]) rTag.ref = grid.NewEntrybox(parts[0]) _, stamp, dur := getGitDateStamp(parts[1]) rTag.date = grid.NewLabel(stamp) rTag.duration = grid.NewLabel(dur) rTag.subject = grid.NewLabel(parts[4]) rTag.deleteB = grid.NewButton("delete", func() { tagversion := parts[0] log.Info("remove tag", tagversion) var all [][]string all = append(all, []string{"git", "tag", "--delete", tagversion}) all = append(all, []string{"git", "push", "--delete", "origin", tagversion}) if rs.DoAll(all) { log.Info("TAG DELETED", rs.String(), tagversion) } else { log.Info("TAG DELETE FAILED", rs.String(), tagversion) } }) tagB.tags = append(tagB.tags, rTag) // works like a typerwriter grid.NextRow() } // reverse the git order // slices.Reverse(rtags.tags) return nil } func (rtags *GitTagBox) ListAll() []*Tag { var tags []*Tag for _, t := range rtags.tags { tags = append(tags, t) } return tags } func (rtags *GitTagBox) List() []*Tag { var tags []*Tag for _, t := range rtags.tags { if t.hidden { // log.Info("tag is hidden", i, t.tag.String()) continue } // log.Info("tag is shown", t.tag.String(), rtags.rs.String()) tags = append(tags, t) } return tags } func (rtags *GitTagBox) Prune() { dups := make(map[string]*Tag) for i, t := range rtags.tags { if t == nil { log.Info("tag empty:", i) continue } ref := t.ref.String() _, ok := dups[ref] if ok { log.Info("tag is duplicate:", i, t.tag.String()) } else { log.Info("new tag", i, t.tag.String()) dups[ref] = t t.Hide() } } } // hide tags worth keeping func (rtags *GitTagBox) PruneSmart() { // always keep the first tag var first bool = true dups := make(map[string]*Tag) for i, t := range rtags.tags { if t == nil { log.Info("tag empty:", i) continue } // check for duplicate tags ref := t.ref.String() _, ok := dups[ref] if ok { log.Info("tag is duplicate:", i, t.tag.String()) continue } dups[ref] = t // dump any tags that don't start with 'v' //if !strings.HasPrefix(t.tag.String(), "v") { // log.Info("tag does not start with v", i, t.tag.String()) // continue //} isVersion := regexp.MustCompile("v[0-9]+.[0-9]+.[0-9]+").MatchString if isVersion(t.tag.String()) { if first { log.Info("keep first tag", i, t.tag.String()) t.Hide() first = false continue } log.Info("valid tag", i, t.tag.String()) t.Hide() continue } if first { log.Info("keep first tag", i, t.tag.String()) t.Hide() first = false continue } log.Info("keep tag", i, t.tag.String()) } } // deleting it locally triggers some but when // the git server was uncontactable (over IPv6 if that matters, probably it doesn't) // and then the local delete re-added it into the tag func (rs *RepoStatus) DeleteTag(rt *Tag) { cmd := []string{"git", "push", "--delete", "origin", rt.tag.String()} log.Info("RUN:", cmd) r := rs.Run(cmd) output := strings.Join(r.Stdout, "\n") if r.Error != nil { log.Info("cmd failed", r.Error) log.Info("output:", output) } log.Info("output:", output) cmd = []string{"git", "tag", "--delete", rt.tag.String()} log.Info("RUN:", cmd) r = rs.Run(cmd) output = strings.Join(r.Stdout, "\n") if r.Error != nil { log.Info("cmd failed", r.Error) log.Info("output:", output) } log.Info("output:", output) } func (rt *Tag) TagString() string { return rt.tag.String() } func (rt *Tag) Hide() { rt.hidden = true rt.tag.Hide() rt.ref.Hide() rt.date.Hide() rt.duration.Hide() rt.subject.Hide() rt.deleteB.Hide() } func (rt *Tag) Show() { rt.hidden = false rt.tag.Show() rt.ref.Show() rt.date.Show() rt.duration.Show() rt.subject.Show() rt.deleteB.Show() } func (rs *RepoStatus) TagExists(findname string) bool { allTags := rs.Tags.ListAll() for _, t := range allTags { tagname := t.TagString() _, filename := filepath.Split(tagname) if filename == findname { // log.Info("found tag:", path, filename, "from", rs.Path()) return true } } return false } func (rs *RepoStatus) LocalTagExists(findname string) bool { allTags := rs.Tags.ListAll() for _, t := range allTags { tagname := t.TagString() if strings.HasPrefix(tagname, "refs/remotes") { continue } path, filename := filepath.Split(tagname) log.Log(INFO, "tag:", path, filename, "from", rs.Path()) if filename == findname { log.Log(INFO, "found tag:", path, filename, "from", rs.Path()) return true } } return false } // returns true if 'taggy' is _ONLY_ a local tag // this means you can not do a git pull or git push on it func (rs *RepoStatus) IsOnlyLocalTag(taggy string) bool { // first make sure the tag is actually even local if !rs.LocalTagExists(taggy) { // this means it's not even local now. return false } // okay, taggy exists, does it exist in a remote repo? for _, t := range rs.Tags.ListAll() { tagname := t.TagString() if strings.HasPrefix(tagname, "refs/remotes") { path, filename := filepath.Split(tagname) if filename == taggy { log.Log(REPOWARN, "found tag:", path, filename, "from", rs.Path()) return false } } } // we couldn't find the local tag anywhere remote, so it's probably only local return true } func (t *Tag) Age() time.Duration { const gitLayout = "Mon Jan 2 15:04:05 2006 -0700" tagTime, _ := time.Parse(gitLayout, t.date.String()) return time.Since(tagTime) } func (t *Tag) Name() string { return t.tag.String() } func (t *Tag) GetDate() (time.Time, error) { const gitLayout = "Mon Jan 2 15:04:05 2006 -0700" tagTime, err := time.Parse(gitLayout, t.date.String()) if err != nil { log.Log(REPOWARN, "tag date err", t.ref.String(), t.tag.String(), err) return time.Now(), err } return tagTime, nil } func (rs *RepoStatus) NewestTag() *Tag { var newest *Tag var newestTime time.Time var tagTime time.Time var err error var allTags []*Tag var mu sync.Mutex allTags = make([]*Tag, 0, 0) junk := rs.Tags.ListAll() allTags = append(allTags, junk...) for _, t := range allTags { mu.Lock() if tagTime, err = t.GetDate(); err != nil { mu.Unlock() continue } // log.Log(REPOWARN, "tag", t.ref.String(), t.date.String(), t.tag.String()) // if this is the first tag, use it if newest == nil { newestTime = tagTime newest = t } // if the tag date is after the newest date, it's newer so use this tag if tagTime.After(newestTime) { newestTime = tagTime newest = t } mu.Unlock() } return newest } func (rs *RepoStatus) DumpTags() { for _, t := range rs.Tags.ListAll() { log.Log(REPOWARN, "tag", t.ref.String(), t.date.String(), t.tag.String()) } }