mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	Update milestone counters when issue is deleted (#21459)
When actions besides "delete" are performed on issues, the milestone counter is updated. However, since deleting issues goes through a different code path, the associated milestone's count wasn't being updated, resulting in inaccurate counts until another issue in the same milestone had a non-delete action performed on it. I verified this change fixes the inaccurate counts using a local docker build. Fixes #21254 Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
		@@ -0,0 +1,19 @@
 | 
			
		||||
# type Milestone struct {
 | 
			
		||||
#   ID              int64                  `xorm:"pk autoincr"`
 | 
			
		||||
#   IsClosed        bool
 | 
			
		||||
#   NumIssues       int
 | 
			
		||||
#   NumClosedIssues int
 | 
			
		||||
#   Completeness    int  // Percentage(1-100).
 | 
			
		||||
# }
 | 
			
		||||
-
 | 
			
		||||
  id: 1
 | 
			
		||||
  is_closed: false
 | 
			
		||||
  num_issues: 3
 | 
			
		||||
  num_closed_issues: 1
 | 
			
		||||
  completeness: 33
 | 
			
		||||
-
 | 
			
		||||
  id: 2
 | 
			
		||||
  is_closed: true
 | 
			
		||||
  num_issues: 5
 | 
			
		||||
  num_closed_issues: 5
 | 
			
		||||
  completeness: 100
 | 
			
		||||
@@ -0,0 +1,25 @@
 | 
			
		||||
# type Issue struct {
 | 
			
		||||
#   ID               int64 `xorm:"pk autoincr"`
 | 
			
		||||
#   RepoID           int64 `xorm:"INDEX UNIQUE(repo_index)"`
 | 
			
		||||
#   Index            int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository.
 | 
			
		||||
#   MilestoneID      int64 `xorm:"INDEX"`
 | 
			
		||||
#   IsClosed         bool  `xorm:"INDEX"`
 | 
			
		||||
# }
 | 
			
		||||
-
 | 
			
		||||
  id: 1
 | 
			
		||||
  repo_id: 1
 | 
			
		||||
  index: 1
 | 
			
		||||
  milestone_id: 1
 | 
			
		||||
  is_closed: false
 | 
			
		||||
-
 | 
			
		||||
  id: 2
 | 
			
		||||
  repo_id: 1
 | 
			
		||||
  index: 2
 | 
			
		||||
  milestone_id: 1
 | 
			
		||||
  is_closed: true
 | 
			
		||||
-
 | 
			
		||||
  id: 4
 | 
			
		||||
  repo_id: 1
 | 
			
		||||
  index: 3
 | 
			
		||||
  milestone_id: 1
 | 
			
		||||
  is_closed: false
 | 
			
		||||
@@ -0,0 +1,19 @@
 | 
			
		||||
# type Milestone struct {
 | 
			
		||||
#   ID              int64                  `xorm:"pk autoincr"`
 | 
			
		||||
#   IsClosed        bool
 | 
			
		||||
#   NumIssues       int
 | 
			
		||||
#   NumClosedIssues int
 | 
			
		||||
#   Completeness    int  // Percentage(1-100).
 | 
			
		||||
# }
 | 
			
		||||
-
 | 
			
		||||
  id: 1
 | 
			
		||||
  is_closed: false
 | 
			
		||||
  num_issues: 4
 | 
			
		||||
  num_closed_issues: 2
 | 
			
		||||
  completeness: 50
 | 
			
		||||
-
 | 
			
		||||
  id: 2
 | 
			
		||||
  is_closed: true
 | 
			
		||||
  num_issues: 5
 | 
			
		||||
  num_closed_issues: 5
 | 
			
		||||
  completeness: 100
 | 
			
		||||
@@ -419,6 +419,8 @@ var migrations = []Migration{
 | 
			
		||||
	NewMigration("Create key/value table for system settings", createSystemSettingsTable),
 | 
			
		||||
	// v228 -> v229
 | 
			
		||||
	NewMigration("Add TeamInvite table", addTeamInviteTable),
 | 
			
		||||
	// v229 -> v230
 | 
			
		||||
	NewMigration("Update counts of all open milestones", updateOpenMilestoneCounts),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetCurrentDBVersion returns the current db version
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										47
									
								
								models/migrations/v229.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								models/migrations/v229.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
// Copyright 2022 The Gitea Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package migrations
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/issues"
 | 
			
		||||
 | 
			
		||||
	"xorm.io/builder"
 | 
			
		||||
	"xorm.io/xorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func updateOpenMilestoneCounts(x *xorm.Engine) error {
 | 
			
		||||
	var openMilestoneIDs []int64
 | 
			
		||||
	err := x.Table("milestone").Select("id").Where(builder.Neq{"is_closed": 1}).Find(&openMilestoneIDs)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("error selecting open milestone IDs: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, id := range openMilestoneIDs {
 | 
			
		||||
		_, err := x.ID(id).
 | 
			
		||||
			SetExpr("num_issues", builder.Select("count(*)").From("issue").Where(
 | 
			
		||||
				builder.Eq{"milestone_id": id},
 | 
			
		||||
			)).
 | 
			
		||||
			SetExpr("num_closed_issues", builder.Select("count(*)").From("issue").Where(
 | 
			
		||||
				builder.Eq{
 | 
			
		||||
					"milestone_id": id,
 | 
			
		||||
					"is_closed":    true,
 | 
			
		||||
				},
 | 
			
		||||
			)).
 | 
			
		||||
			Update(&issues.Milestone{})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("error updating issue counts in milestone %d: %w", id, err)
 | 
			
		||||
		}
 | 
			
		||||
		_, err = x.Exec("UPDATE `milestone` SET completeness=100*num_closed_issues/(CASE WHEN num_issues > 0 THEN num_issues ELSE 1 END) WHERE id=?",
 | 
			
		||||
			id,
 | 
			
		||||
		)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("error setting completeness on milestone %d: %w", id, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										46
									
								
								models/migrations/v229_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								models/migrations/v229_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
			
		||||
// Copyright 2022 The Gitea Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package migrations
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/issues"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func Test_updateOpenMilestoneCounts(t *testing.T) {
 | 
			
		||||
	type ExpectedMilestone issues.Milestone
 | 
			
		||||
 | 
			
		||||
	// Prepare and load the testing database
 | 
			
		||||
	x, deferable := prepareTestEnv(t, 0, new(issues.Milestone), new(ExpectedMilestone), new(issues.Issue))
 | 
			
		||||
	defer deferable()
 | 
			
		||||
	if x == nil || t.Failed() {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := updateOpenMilestoneCounts(x); err != nil {
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expected := []ExpectedMilestone{}
 | 
			
		||||
	if err := x.Table("expected_milestone").Asc("id").Find(&expected); !assert.NoError(t, err) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	got := []issues.Milestone{}
 | 
			
		||||
	if err := x.Table("milestone").Asc("id").Find(&got); !assert.NoError(t, err) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i, e := range expected {
 | 
			
		||||
		got := got[i]
 | 
			
		||||
		assert.Equal(t, e.ID, got.ID)
 | 
			
		||||
		assert.Equal(t, e.NumIssues, got.NumIssues)
 | 
			
		||||
		assert.Equal(t, e.NumClosedIssues, got.NumClosedIssues)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -224,6 +224,11 @@ func deleteIssue(issue *issues_model.Issue) error {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := issues_model.UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil {
 | 
			
		||||
		return fmt.Errorf("error updating counters for milestone id %d: %w",
 | 
			
		||||
			issue.MilestoneID, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := activities_model.DeleteIssueActions(ctx, issue.RepoID, issue.ID); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user