Let remove be a remove (#593)

* UI: let remove be a remove
* UI: add deactivate repo btn
* Store: DeleteRepo also delete related
* Store: more test coverage

Co-authored-by: 6543 <6543@obermui.de>
This commit is contained in:
Anbraten 2021-12-11 16:03:14 +01:00 committed by GitHub
parent 4cbdacb21c
commit fe6c999160
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 163 additions and 23 deletions

View file

@ -199,7 +199,6 @@ func GetRepoBranches(c *gin.Context) {
func DeleteRepo(c *gin.Context) {
remove, _ := strconv.ParseBool(c.Query("remove"))
remote := server.Config.Services.Remote
_store := store.FromContext(c)
repo := session.Repo(c)
@ -208,21 +207,19 @@ func DeleteRepo(c *gin.Context) {
repo.IsActive = false
repo.UserID = 0
err := _store.UpdateRepo(repo)
if err != nil {
if err := _store.UpdateRepo(repo); err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
if remove {
err := _store.DeleteRepo(repo)
if err != nil {
if err := _store.DeleteRepo(repo); err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
}
if err := remote.Deactivate(c, user, repo, server.Config.Server.Host); err != nil {
if err := server.Config.Services.Remote.Deactivate(c, user, repo, server.Config.Server.Host); err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}

View file

@ -18,4 +18,5 @@ type Logs struct {
ID int64 `xorm:"pk autoincr 'log_id'"`
ProcID int64 `xorm:"UNIQUE 'log_job_id'"`
Data []byte `xorm:"log_data"`
// TODO: add create timestamp
}

View file

@ -17,6 +17,8 @@ package datastore
import (
"time"
"xorm.io/xorm"
"github.com/woodpecker-ci/woodpecker/server/model"
)
@ -108,6 +110,7 @@ func (s storage) CreateBuild(build *model.Build, procList ...*model.Proc) error
}
for i := range procList {
procList[i].BuildID = build.ID
// only Insert set auto created ID back to object
if _, err := sess.Insert(procList[i]); err != nil {
return err
@ -121,3 +124,27 @@ func (s storage) UpdateBuild(build *model.Build) error {
_, err := s.engine.ID(build.ID).AllCols().Update(build)
return err
}
func deleteBuild(sess *xorm.Session, buildID int64) error {
// delete related procs
for startProcs := 0; ; startProcs += perPage {
procIDs := make([]int64, 0, perPage)
if err := sess.Limit(perPage, startProcs).Table("procs").Cols("proc_id").Where("proc_build_id = ?", buildID).Find(&procIDs); err != nil {
return err
}
if len(procIDs) == 0 {
break
}
for i := range procIDs {
if err := deleteProc(sess, procIDs[i]); err != nil {
return err
}
}
}
if _, err := sess.Where("build_id = ?", buildID).Delete(new(model.BuildConfig)); err != nil {
return err
}
_, err := sess.ID(buildID).Delete(new(model.Build))
return err
}

View file

@ -15,6 +15,8 @@
package datastore
import (
"xorm.io/xorm"
"github.com/woodpecker-ci/woodpecker/server/model"
)
@ -84,3 +86,14 @@ func (s storage) ProcClear(build *model.Build) error {
return sess.Commit()
}
func deleteProc(sess *xorm.Session, procID int64) error {
if _, err := sess.Where("log_job_id = ?", procID).Delete(new(model.Logs)); err != nil {
return err
}
if _, err := sess.Where("file_proc_id = ?", procID).Delete(new(model.File)); err != nil {
return err
}
_, err := sess.ID(procID).Delete(new(model.Proc))
return err
}

View file

@ -54,9 +54,51 @@ func (s storage) UpdateRepo(repo *model.Repo) error {
}
func (s storage) DeleteRepo(repo *model.Repo) error {
_, err := s.engine.ID(repo.ID).Delete(new(model.Repo))
// TODO: delete related within a session
return err
const batchSize = perPage
sess := s.engine.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
if _, err := sess.Where("sender_repo_id = ?", repo.ID).Delete(new(model.Sender)); err != nil {
return err
}
if _, err := sess.Where("config_repo_id = ?", repo.ID).Delete(new(model.Config)); err != nil {
return err
}
if _, err := sess.Where("perm_repo_id = ?", repo.ID).Delete(new(model.Perm)); err != nil {
return err
}
if _, err := sess.Where("registry_repo_id = ?", repo.ID).Delete(new(model.Registry)); err != nil {
return err
}
if _, err := sess.Where("secret_repo_id = ?", repo.ID).Delete(new(model.Secret)); err != nil {
return err
}
// delete related builds
for startBuilds := 0; ; startBuilds += batchSize {
buildIDs := make([]int64, 0, batchSize)
if err := sess.Limit(batchSize, startBuilds).Table("builds").Cols("build_id").Where("build_repo_id = ?", repo.ID).Find(&buildIDs); err != nil {
return err
}
if len(buildIDs) == 0 {
break
}
for i := range buildIDs {
if err := deleteBuild(sess, buildIDs[i]); err != nil {
return err
}
}
}
if _, err := sess.ID(repo.ID).Delete(new(model.Repo)); err != nil {
return err
}
return sess.Commit()
}
// RepoList list all repos where permissions fo specific user are stored

View file

@ -362,7 +362,19 @@ func TestRepoBatch(t *testing.T) {
}
func TestRepoCrud(t *testing.T) {
store, closer := newTestStore(t, new(model.Repo), new(model.User), new(model.Perm))
store, closer := newTestStore(t,
new(model.Repo),
new(model.User),
new(model.Perm),
new(model.Build),
new(model.BuildConfig),
new(model.Logs),
new(model.Proc),
new(model.File),
new(model.Secret),
new(model.Sender),
new(model.Registry),
new(model.Config))
defer closer()
repo := model.Repo{
@ -372,16 +384,40 @@ func TestRepoCrud(t *testing.T) {
Name: "test",
}
assert.NoError(t, store.CreateRepo(&repo))
_, err1 := store.GetRepo(repo.ID)
err2 := store.DeleteRepo(&repo)
_, err3 := store.GetRepo(repo.ID)
if err1 != nil {
t.Errorf("Unexpected error: select repository: %s", err1)
build := model.Build{
RepoID: repo.ID,
}
if err2 != nil {
t.Errorf("Unexpected error: delete repository: %s", err2)
proc := model.Proc{
Name: "a proc",
}
if err3 == nil {
t.Errorf("Expected error: sql.ErrNoRows")
assert.NoError(t, store.CreateBuild(&build, &proc))
// create unrelated
repoUnrelated := model.Repo{
UserID: 2,
FullName: "x/x",
Owner: "x",
Name: "x",
}
assert.NoError(t, store.CreateRepo(&repoUnrelated))
buildUnrelated := model.Build{
RepoID: repoUnrelated.ID,
}
procUnrelated := model.Proc{
Name: "a unrelated proc",
}
assert.NoError(t, store.CreateBuild(&buildUnrelated, &procUnrelated))
_, err := store.GetRepo(repo.ID)
assert.NoError(t, err)
assert.NoError(t, store.DeleteRepo(&repo))
_, err = store.GetRepo(repo.ID)
assert.Error(t, err)
procCount, err := store.engine.Count(new(model.Proc))
assert.NoError(t, err)
assert.EqualValues(t, 1, procCount)
buildCount, err := store.engine.Count(new(model.Build))
assert.NoError(t, err)
assert.EqualValues(t, 1, buildCount)
}

View file

@ -30,6 +30,7 @@
<i-ic-round-light-mode v-else-if="name === 'light'" class="h-6 w-6" />
<i-mdi-sync v-else-if="name === 'sync'" class="h-6 w-6" />
<i-ic-baseline-healing v-else-if="name === 'heal'" class="h-6 w-6" />
<i-bx-bx-power-off v-else-if="name === 'turn-off'" class="h-6 w-6" />
<div v-else-if="name === 'blank'" class="h-6 w-6" />
</template>
@ -68,7 +69,8 @@ export type IconNames =
| 'dark'
| 'light'
| 'sync'
| 'heal';
| 'heal'
| 'turn-off';
export default defineComponent({
name: 'Icon',

View file

@ -14,6 +14,15 @@
@click="repairRepo"
/>
<Button
class="mr-auto mt-4"
color="blue"
start-icon="turn-off"
text="Disable repository"
:is-loading="isDeactivatingRepo"
@click="deactivateRepo"
/>
<Button
class="mr-auto mt-4"
color="red"
@ -65,7 +74,7 @@ export default defineComponent({
// TODO use proper dialog
// eslint-disable-next-line no-alert, no-restricted-globals
if (!confirm('All data will be lost after this action!!!\n\nDo you really want to procceed?')) {
if (!confirm('All data will be lost after this action!!!\n\nDo you really want to proceed?')) {
return;
}
@ -74,11 +83,23 @@ export default defineComponent({
await router.replace({ name: 'repos' });
});
const { doSubmit: deactivateRepo, isLoading: isDeactivatingRepo } = useAsyncAction(async () => {
if (!repo) {
throw new Error('Unexpected: Repo should be set');
}
await apiClient.deleteRepo(repo.value.owner, repo.value.name, false);
notifications.notify({ title: 'Repository disabled', type: 'success' });
await router.replace({ name: 'repos' });
});
return {
isRepairingRepo,
isDeletingRepo,
isDeactivatingRepo,
deleteRepo,
repairRepo,
deactivateRepo,
};
},
});

View file

@ -31,8 +31,9 @@ export default class WoodpeckerClient extends ApiClient {
return this._patch(`/api/repos/${owner}/${repo}`, repoSettings);
}
deleteRepo(owner: string, repo: string): Promise<unknown> {
return this._delete(`/api/repos/${owner}/${repo}`);
deleteRepo(owner: string, repo: string, remove = true): Promise<unknown> {
const query = encodeQueryString({ remove });
return this._delete(`/api/repos/${owner}/${repo}?${query}`);
}
repairRepo(owner: string, repo: string): Promise<unknown> {