fix(tags): Resolve tag deletion bug in remove-task command
Refactored the core 'removeTask' function to be fully tag-aware, preventing data corruption. - The function now correctly reads the full tagged data structure by prioritizing '_rawTaggedData' instead of operating on a resolved single-tag view. - All subsequent operations (task removal, dependency cleanup, file writing) now correctly reference the full multi-tag data object, preserving the integrity of 'tasks.json'. - This resolves the critical bug where removing a task would delete all other tags.
This commit is contained in:
@@ -9,9 +9,11 @@ import taskExists from './task-exists.js';
|
||||
* Removes one or more tasks or subtasks from the tasks file
|
||||
* @param {string} tasksPath - Path to the tasks file
|
||||
* @param {string} taskIds - Comma-separated string of task/subtask IDs to remove (e.g., '5,6.1,7')
|
||||
* @param {Object} context - Context object containing projectRoot and tag information
|
||||
* @returns {Object} Result object with success status, messages, and removed task info
|
||||
*/
|
||||
async function removeTask(tasksPath, taskIds) {
|
||||
async function removeTask(tasksPath, taskIds, context = {}) {
|
||||
const { projectRoot, tag } = context;
|
||||
const results = {
|
||||
success: true,
|
||||
messages: [],
|
||||
@@ -30,18 +32,28 @@ async function removeTask(tasksPath, taskIds) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Read the tasks file ONCE before the loop
|
||||
const data = readJSON(tasksPath);
|
||||
if (!data || !data.tasks) {
|
||||
throw new Error(`No valid tasks found in ${tasksPath}`);
|
||||
// Read the tasks file ONCE before the loop, preserving the full tagged structure
|
||||
const rawData = readJSON(tasksPath, projectRoot); // Read raw data
|
||||
if (!rawData) {
|
||||
throw new Error(`Could not read tasks file at ${tasksPath}`);
|
||||
}
|
||||
|
||||
// Use the full tagged data if available, otherwise use the data as is
|
||||
const fullTaggedData = rawData._rawTaggedData || rawData;
|
||||
|
||||
const currentTag = tag || rawData.tag || 'master';
|
||||
if (!fullTaggedData[currentTag] || !fullTaggedData[currentTag].tasks) {
|
||||
throw new Error(`Tag '${currentTag}' not found or has no tasks.`);
|
||||
}
|
||||
|
||||
const tasks = fullTaggedData[currentTag].tasks; // Work with tasks from the correct tag
|
||||
|
||||
const tasksToDeleteFiles = []; // Collect IDs of main tasks whose files should be deleted
|
||||
|
||||
for (const taskId of taskIdsToRemove) {
|
||||
// Check if the task ID exists *before* attempting removal
|
||||
if (!taskExists(data.tasks, taskId)) {
|
||||
const errorMsg = `Task with ID ${taskId} not found or already removed.`;
|
||||
if (!taskExists(tasks, taskId)) {
|
||||
const errorMsg = `Task with ID ${taskId} in tag '${currentTag}' not found or already removed.`;
|
||||
results.errors.push(errorMsg);
|
||||
results.success = false; // Mark overall success as false if any error occurs
|
||||
continue; // Skip to the next ID
|
||||
@@ -55,7 +67,7 @@ async function removeTask(tasksPath, taskIds) {
|
||||
.map((id) => parseInt(id, 10));
|
||||
|
||||
// Find the parent task
|
||||
const parentTask = data.tasks.find((t) => t.id === parentTaskId);
|
||||
const parentTask = tasks.find((t) => t.id === parentTaskId);
|
||||
if (!parentTask || !parentTask.subtasks) {
|
||||
throw new Error(
|
||||
`Parent task ${parentTaskId} or its subtasks not found for subtask ${taskId}`
|
||||
@@ -82,27 +94,31 @@ async function removeTask(tasksPath, taskIds) {
|
||||
// Remove the subtask from the parent
|
||||
parentTask.subtasks.splice(subtaskIndex, 1);
|
||||
|
||||
results.messages.push(`Successfully removed subtask ${taskId}`);
|
||||
results.messages.push(
|
||||
`Successfully removed subtask ${taskId} from tag '${currentTag}'`
|
||||
);
|
||||
}
|
||||
// Handle main task removal
|
||||
else {
|
||||
const taskIdNum = parseInt(taskId, 10);
|
||||
const taskIndex = data.tasks.findIndex((t) => t.id === taskIdNum);
|
||||
const taskIndex = tasks.findIndex((t) => t.id === taskIdNum);
|
||||
if (taskIndex === -1) {
|
||||
// This case should theoretically be caught by the taskExists check above,
|
||||
// but keep it as a safeguard.
|
||||
throw new Error(`Task with ID ${taskId} not found`);
|
||||
throw new Error(
|
||||
`Task with ID ${taskId} not found in tag '${currentTag}'`
|
||||
);
|
||||
}
|
||||
|
||||
// Store the task info before removal
|
||||
const removedTask = data.tasks[taskIndex];
|
||||
const removedTask = tasks[taskIndex];
|
||||
results.removedTasks.push(removedTask);
|
||||
tasksToDeleteFiles.push(taskIdNum); // Add to list for file deletion
|
||||
|
||||
// Remove the task from the main array
|
||||
data.tasks.splice(taskIndex, 1);
|
||||
tasks.splice(taskIndex, 1);
|
||||
|
||||
results.messages.push(`Successfully removed task ${taskId}`);
|
||||
results.messages.push(
|
||||
`Successfully removed task ${taskId} from tag '${currentTag}'`
|
||||
);
|
||||
}
|
||||
} catch (innerError) {
|
||||
// Catch errors specific to processing *this* ID
|
||||
@@ -117,36 +133,46 @@ async function removeTask(tasksPath, taskIds) {
|
||||
|
||||
// Only proceed with cleanup and saving if at least one task was potentially removed
|
||||
if (results.removedTasks.length > 0) {
|
||||
// Remove all references AFTER all tasks/subtasks are removed
|
||||
const allRemovedIds = new Set(
|
||||
taskIdsToRemove.map((id) =>
|
||||
typeof id === 'string' && id.includes('.') ? id : parseInt(id, 10)
|
||||
)
|
||||
);
|
||||
|
||||
data.tasks.forEach((task) => {
|
||||
// Clean dependencies in main tasks
|
||||
if (task.dependencies) {
|
||||
task.dependencies = task.dependencies.filter(
|
||||
(depId) => !allRemovedIds.has(depId)
|
||||
);
|
||||
}
|
||||
// Clean dependencies in remaining subtasks
|
||||
if (task.subtasks) {
|
||||
task.subtasks.forEach((subtask) => {
|
||||
if (subtask.dependencies) {
|
||||
subtask.dependencies = subtask.dependencies.filter(
|
||||
(depId) =>
|
||||
!allRemovedIds.has(`${task.id}.${depId}`) &&
|
||||
!allRemovedIds.has(depId) // check both subtask and main task refs
|
||||
// Update the tasks in the current tag of the full data structure
|
||||
fullTaggedData[currentTag].tasks = tasks;
|
||||
|
||||
// Remove dependencies from all tags
|
||||
for (const tagName in fullTaggedData) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(fullTaggedData, tagName) &&
|
||||
fullTaggedData[tagName] &&
|
||||
fullTaggedData[tagName].tasks
|
||||
) {
|
||||
const currentTagTasks = fullTaggedData[tagName].tasks;
|
||||
currentTagTasks.forEach((task) => {
|
||||
if (task.dependencies) {
|
||||
task.dependencies = task.dependencies.filter(
|
||||
(depId) => !allRemovedIds.has(depId)
|
||||
);
|
||||
}
|
||||
if (task.subtasks) {
|
||||
task.subtasks.forEach((subtask) => {
|
||||
if (subtask.dependencies) {
|
||||
subtask.dependencies = subtask.dependencies.filter(
|
||||
(depId) =>
|
||||
!allRemovedIds.has(`${task.id}.${depId}`) &&
|
||||
!allRemovedIds.has(depId)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Save the updated tasks file ONCE
|
||||
writeJSON(tasksPath, data);
|
||||
// Save the updated raw data structure
|
||||
writeJSON(tasksPath, fullTaggedData);
|
||||
|
||||
// Delete task files AFTER saving tasks.json
|
||||
for (const taskIdNum of tasksToDeleteFiles) {
|
||||
@@ -167,9 +193,12 @@ async function removeTask(tasksPath, taskIds) {
|
||||
}
|
||||
}
|
||||
|
||||
// Generate updated task files ONCE
|
||||
// Generate updated task files ONCE, with context
|
||||
try {
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
|
||||
projectRoot,
|
||||
tag: currentTag
|
||||
});
|
||||
results.messages.push('Task files regenerated successfully.');
|
||||
} catch (genError) {
|
||||
const genErrMsg = `Failed to regenerate task files: ${genError.message}`;
|
||||
@@ -178,7 +207,6 @@ async function removeTask(tasksPath, taskIds) {
|
||||
log('warn', genErrMsg);
|
||||
}
|
||||
} else if (results.errors.length === 0) {
|
||||
// Case where valid IDs were provided but none existed
|
||||
results.messages.push('No tasks found matching the provided IDs.');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user