mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +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),
 | 
						NewMigration("Create key/value table for system settings", createSystemSettingsTable),
 | 
				
			||||||
	// v228 -> v229
 | 
						// v228 -> v229
 | 
				
			||||||
	NewMigration("Add TeamInvite table", addTeamInviteTable),
 | 
						NewMigration("Add TeamInvite table", addTeamInviteTable),
 | 
				
			||||||
 | 
						// v229 -> v230
 | 
				
			||||||
 | 
						NewMigration("Update counts of all open milestones", updateOpenMilestoneCounts),
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetCurrentDBVersion returns the current db version
 | 
					// 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
 | 
							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 {
 | 
						if err := activities_model.DeleteIssueActions(ctx, issue.RepoID, issue.ID); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user