From 71657fce30cf43aae8f2b15a9bf949e2727ee328 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 25 Feb 2026 11:30:15 +0100 Subject: [PATCH] feat: add repair-projects CLI command --- pkg/cmd/repair_projects.go | 80 +++++++++++++++++++++++++++++++ pkg/models/project_repair.go | 1 - pkg/models/project_repair_test.go | 1 - 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 pkg/cmd/repair_projects.go diff --git a/pkg/cmd/repair_projects.go b/pkg/cmd/repair_projects.go new file mode 100644 index 000000000..914d03308 --- /dev/null +++ b/pkg/cmd/repair_projects.go @@ -0,0 +1,80 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "code.vikunja.io/api/pkg/db" + "code.vikunja.io/api/pkg/initialize" + "code.vikunja.io/api/pkg/log" + "code.vikunja.io/api/pkg/models" + + "github.com/spf13/cobra" +) + +func init() { + repairProjectsCmd.Flags().Bool("dry-run", false, "Preview repairs without making changes") + rootCmd.AddCommand(repairProjectsCmd) +} + +var repairProjectsCmd = &cobra.Command{ + Use: "repair-projects", + Short: "Repair orphaned projects whose parent project no longer exists", + Long: `Finds projects whose parent_project_id references a project that no longer +exists in the database and re-parents them to the top level (parent_project_id = 0). + +This can happen when a parent project is deleted but its sub-projects are not +fully cleaned up, for example after importing from external services like Trello. + +Orphaned projects cannot be un-archived, modified, or deleted through the UI +because permission checks fail when traversing the broken parent chain. + +Use --dry-run to preview what would be fixed without making changes.`, + PreRun: func(_ *cobra.Command, _ []string) { + initialize.FullInitWithoutAsync() + }, + Run: func(cmd *cobra.Command, _ []string) { + dryRun, _ := cmd.Flags().GetBool("dry-run") + + s := db.NewSession() + defer s.Close() + + if dryRun { + log.Infof("Running in dry-run mode - no changes will be made") + } + + result, err := models.RepairOrphanedProjects(s, dryRun) + if err != nil { + log.Errorf("Failed to repair orphaned projects: %s", err) + return + } + + if !dryRun { + if err := s.Commit(); err != nil { + log.Errorf("Failed to commit changes: %s", err) + return + } + } + + log.Infof("Repair complete:") + log.Infof(" Orphaned projects found: %d", result.Found) + log.Infof(" Projects repaired: %d", result.Repaired) + + if result.Found == 0 { + log.Infof("No orphaned projects found - all parent references are valid!") + } + }, +} diff --git a/pkg/models/project_repair.go b/pkg/models/project_repair.go index 7f108d39f..49e641755 100644 --- a/pkg/models/project_repair.go +++ b/pkg/models/project_repair.go @@ -68,4 +68,3 @@ func RepairOrphanedProjects(s *xorm.Session, dryRun bool) (*RepairOrphanedProjec return result, nil } - diff --git a/pkg/models/project_repair_test.go b/pkg/models/project_repair_test.go index 5ba911833..65f2385fb 100644 --- a/pkg/models/project_repair_test.go +++ b/pkg/models/project_repair_test.go @@ -80,4 +80,3 @@ func TestRepairOrphanedProjects(t *testing.T) { assert.Equal(t, 0, result.Repaired) }) } -