Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Optimize 'refreshAccesses' to perform update without removing then adding #35702

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
rossigee wants to merge 7 commits into go-gitea:main
base: main
Choose a base branch
Loading
from rossigee:optimize-access-refresh-clean
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 60 additions & 15 deletions models/perm/access/access.go
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ func updateUserAccess(accessMap map[int64]*userAccess, user *user_model.User, mo
}
}

// FIXME: do cross-comparison so reduce deletions and additions to the minimum?
func refreshAccesses(ctx context.Context, repo *repo_model.Repository, accessMap map[int64]*userAccess) (err error) {
minMode := perm.AccessModeRead
if err := repo.LoadOwner(ctx); err != nil {
Expand All @@ -104,30 +103,76 @@ func refreshAccesses(ctx context.Context, repo *repo_model.Repository, accessMap
minMode = perm.AccessModeWrite
}

newAccesses := make([]Access, 0, len(accessMap))
// Query existing accesses for cross-comparison
var existingAccesses []Access
if err := db.GetEngine(ctx).Where(builder.Eq{"repo_id": repo.ID}).Find(&existingAccesses); err != nil {
return fmt.Errorf("find existing accesses: %w", err)
}
existingMap := make(map[int64]perm.AccessMode, len(existingAccesses))
for _, a := range existingAccesses {
existingMap[a.UserID] = a.Mode
}

var toDelete []int64
var toInsert []Access
var toUpdate []struct {
UserID int64
Mode perm.AccessMode
}
Comment on lines +118 to +121
Copy link
Contributor

@wxiaoguang wxiaoguang Oct 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can reuse the Access struct, and it could be clearer for the code below


// Determine changes
for userID, ua := range accessMap {
if ua.Mode < minMode && !ua.User.IsRestricted {
continue
// Should not have access
Copy link
Contributor

@wxiaoguang wxiaoguang Oct 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment seems not right.

If I read correctly, it doesn't mean "Should not have access", but "default access" is enough?


If it really means "should not have access", why ua.User.IsRestricted is kept

Copy link
Contributor

@wxiaoguang wxiaoguang Oct 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a test:

  1. Add U1 as public repo's collaborator with "read" access
  2. Trigger "refreshAccesses"
  3. U1's "access" record is skipped
  4. U1 can still access the public repo because the repo is public

if _, exists := existingMap[userID]; exists {
toDelete = append(toDelete, userID)
}
} else {
desiredMode := ua.Mode
if existingMode, exists := existingMap[userID]; exists {
if existingMode != desiredMode {
toUpdate = append(toUpdate, struct {
UserID int64
Mode perm.AccessMode
}{UserID: userID, Mode: desiredMode})
}
} else {
toInsert = append(toInsert, Access{
UserID: userID,
RepoID: repo.ID,
Mode: desiredMode,
})
}
}
delete(existingMap, userID)
}

newAccesses = append(newAccesses, Access{
UserID: userID,
RepoID: repo.ID,
Mode: ua.Mode,
})
// Remaining in existingMap should be deleted
for userID := range existingMap {
toDelete = append(toDelete, userID)
}

// Delete old accesses and insert new ones for repository.
if _, err = db.DeleteByBean(ctx, &Access{RepoID: repo.ID}); err != nil {
return fmt.Errorf("delete old accesses: %w", err)
// Execute deletions
if len(toDelete) > 0 {
if _, err = db.GetEngine(ctx).In("user_id", toDelete).And("repo_id = ?", repo.ID).Delete(&Access{}); err != nil {
return fmt.Errorf("delete accesses: %w", err)
}
}
if len(newAccesses) == 0 {
return nil

// Execute updates
for _, u := range toUpdate {
if _, err = db.GetEngine(ctx).Where("user_id = ? AND repo_id = ?", u.UserID, repo.ID).Cols("mode").Update(&Access{Mode: u.Mode}); err != nil {
return fmt.Errorf("update access for user %d: %w", u.UserID, err)
}
}

if err = db.Insert(ctx, newAccesses); err != nil {
return fmt.Errorf("insert new accesses: %w", err)
// Execute insertions
if len(toInsert) > 0 {
if err = db.Insert(ctx, toInsert); err != nil {
return fmt.Errorf("insert new accesses: %w", err)
}
}

return nil
}

Expand Down
35 changes: 35 additions & 0 deletions models/perm/access/access_test.go
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,38 @@ func TestRepository_RecalculateAccesses2(t *testing.T) {
assert.NoError(t, err)
assert.False(t, has)
}

func TestRepository_RecalculateAccesses_UpdateMode(t *testing.T) {
// Test the update path in refreshAccesses optimization
// Scenario: User's access mode changes from Read to Write
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
assert.NoError(t, repo.LoadOwner(t.Context()))

// Verify initial access mode
initialAccess := &access_model.Access{UserID: 4, RepoID: 4}
has, err := db.GetEngine(t.Context()).Get(initialAccess)
assert.NoError(t, err)
assert.True(t, has)
initialMode := initialAccess.Mode

// Change collaboration mode to trigger update path
newMode := perm_model.AccessModeAdmin
assert.NotEqual(t, initialMode, newMode, "New mode should differ from initial mode")

_, err = db.GetEngine(t.Context()).
Where("user_id = ? AND repo_id = ?", 4, 4).
Cols("mode").
Update(&repo_model.Collaboration{Mode: newMode})
assert.NoError(t, err)

// Recalculate accesses - should UPDATE existing access, not delete+insert
assert.NoError(t, access_model.RecalculateAccesses(t.Context(), repo))

// Verify access was updated, not deleted and re-inserted
updatedAccess := &access_model.Access{UserID: 4, RepoID: 4}
has, err = db.GetEngine(t.Context()).Get(updatedAccess)
assert.NoError(t, err)
assert.True(t, has, "Access should still exist")
assert.Equal(t, newMode, updatedAccess.Mode, "Access mode should be updated to new collaboration mode")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to have another use case to remove this user's permission and have a test again.

}

AltStyle によって変換されたページ (->オリジナル) /