mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
Merge pull request #468 from AutoMaker-Org/feat/add-branch-name-to-mass-edit
feat: add branch/worktree support to mass edit dialog
This commit is contained in:
@@ -129,10 +129,30 @@ export const TOOL_PRESETS = {
|
|||||||
specGeneration: ['Read', 'Glob', 'Grep'] as const,
|
specGeneration: ['Read', 'Glob', 'Grep'] as const,
|
||||||
|
|
||||||
/** Full tool access for feature implementation */
|
/** Full tool access for feature implementation */
|
||||||
fullAccess: ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch', 'TodoWrite'] as const,
|
fullAccess: [
|
||||||
|
'Read',
|
||||||
|
'Write',
|
||||||
|
'Edit',
|
||||||
|
'Glob',
|
||||||
|
'Grep',
|
||||||
|
'Bash',
|
||||||
|
'WebSearch',
|
||||||
|
'WebFetch',
|
||||||
|
'TodoWrite',
|
||||||
|
] as const,
|
||||||
|
|
||||||
/** Tools for chat/interactive mode */
|
/** Tools for chat/interactive mode */
|
||||||
chat: ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch', 'TodoWrite'] as const,
|
chat: [
|
||||||
|
'Read',
|
||||||
|
'Write',
|
||||||
|
'Edit',
|
||||||
|
'Glob',
|
||||||
|
'Grep',
|
||||||
|
'Bash',
|
||||||
|
'WebSearch',
|
||||||
|
'WebFetch',
|
||||||
|
'TodoWrite',
|
||||||
|
] as const,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -422,6 +422,31 @@ export function BoardView() {
|
|||||||
const selectedWorktreeBranch =
|
const selectedWorktreeBranch =
|
||||||
currentWorktreeBranch || worktrees.find((w) => w.isMain)?.branch || 'main';
|
currentWorktreeBranch || worktrees.find((w) => w.isMain)?.branch || 'main';
|
||||||
|
|
||||||
|
// Helper function to add and select a worktree
|
||||||
|
const addAndSelectWorktree = useCallback(
|
||||||
|
(worktreeResult: { path: string; branch: string }) => {
|
||||||
|
if (!currentProject) return;
|
||||||
|
|
||||||
|
const currentWorktrees = getWorktrees(currentProject.path);
|
||||||
|
const existingWorktree = currentWorktrees.find((w) => w.branch === worktreeResult.branch);
|
||||||
|
|
||||||
|
// Only add if it doesn't already exist (to avoid duplicates)
|
||||||
|
if (!existingWorktree) {
|
||||||
|
const newWorktreeInfo = {
|
||||||
|
path: worktreeResult.path,
|
||||||
|
branch: worktreeResult.branch,
|
||||||
|
isMain: false,
|
||||||
|
isCurrent: false,
|
||||||
|
hasWorktree: true,
|
||||||
|
};
|
||||||
|
setWorktrees(currentProject.path, [...currentWorktrees, newWorktreeInfo]);
|
||||||
|
}
|
||||||
|
// Select the worktree (whether it existed or was just added)
|
||||||
|
setCurrentWorktree(currentProject.path, worktreeResult.path, worktreeResult.branch);
|
||||||
|
},
|
||||||
|
[currentProject, getWorktrees, setWorktrees, setCurrentWorktree]
|
||||||
|
);
|
||||||
|
|
||||||
// Extract all action handlers into a hook
|
// Extract all action handlers into a hook
|
||||||
const {
|
const {
|
||||||
handleAddFeature,
|
handleAddFeature,
|
||||||
@@ -467,43 +492,90 @@ export function BoardView() {
|
|||||||
outputFeature,
|
outputFeature,
|
||||||
projectPath: currentProject?.path || null,
|
projectPath: currentProject?.path || null,
|
||||||
onWorktreeCreated: () => setWorktreeRefreshKey((k) => k + 1),
|
onWorktreeCreated: () => setWorktreeRefreshKey((k) => k + 1),
|
||||||
onWorktreeAutoSelect: (newWorktree) => {
|
onWorktreeAutoSelect: addAndSelectWorktree,
|
||||||
if (!currentProject) return;
|
|
||||||
// Check if worktree already exists in the store (by branch name)
|
|
||||||
const currentWorktrees = getWorktrees(currentProject.path);
|
|
||||||
const existingWorktree = currentWorktrees.find((w) => w.branch === newWorktree.branch);
|
|
||||||
|
|
||||||
// Only add if it doesn't already exist (to avoid duplicates)
|
|
||||||
if (!existingWorktree) {
|
|
||||||
const newWorktreeInfo = {
|
|
||||||
path: newWorktree.path,
|
|
||||||
branch: newWorktree.branch,
|
|
||||||
isMain: false,
|
|
||||||
isCurrent: false,
|
|
||||||
hasWorktree: true,
|
|
||||||
};
|
|
||||||
setWorktrees(currentProject.path, [...currentWorktrees, newWorktreeInfo]);
|
|
||||||
}
|
|
||||||
// Select the worktree (whether it existed or was just added)
|
|
||||||
setCurrentWorktree(currentProject.path, newWorktree.path, newWorktree.branch);
|
|
||||||
},
|
|
||||||
currentWorktreeBranch,
|
currentWorktreeBranch,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handler for bulk updating multiple features
|
// Handler for bulk updating multiple features
|
||||||
const handleBulkUpdate = useCallback(
|
const handleBulkUpdate = useCallback(
|
||||||
async (updates: Partial<Feature>) => {
|
async (updates: Partial<Feature>, workMode: 'current' | 'auto' | 'custom') => {
|
||||||
if (!currentProject || selectedFeatureIds.size === 0) return;
|
if (!currentProject || selectedFeatureIds.size === 0) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Determine final branch name based on work mode:
|
||||||
|
// - 'current': No branch name, work on current branch (no worktree)
|
||||||
|
// - 'auto': Auto-generate branch name based on current branch
|
||||||
|
// - 'custom': Use the provided branch name
|
||||||
|
let finalBranchName: string | undefined;
|
||||||
|
|
||||||
|
if (workMode === 'current') {
|
||||||
|
// No worktree isolation - work directly on current branch
|
||||||
|
finalBranchName = undefined;
|
||||||
|
} else if (workMode === 'auto') {
|
||||||
|
// Auto-generate a branch name based on current branch and timestamp
|
||||||
|
const baseBranch =
|
||||||
|
currentWorktreeBranch || getPrimaryWorktreeBranch(currentProject.path) || 'main';
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const randomSuffix = Math.random().toString(36).substring(2, 6);
|
||||||
|
finalBranchName = `feature/${baseBranch}-${timestamp}-${randomSuffix}`;
|
||||||
|
} else {
|
||||||
|
// Custom mode - use provided branch name
|
||||||
|
finalBranchName = updates.branchName || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create worktree for 'auto' or 'custom' modes when we have a branch name
|
||||||
|
if ((workMode === 'auto' || workMode === 'custom') && finalBranchName) {
|
||||||
|
try {
|
||||||
|
const electronApi = getElectronAPI();
|
||||||
|
if (electronApi?.worktree?.create) {
|
||||||
|
const result = await electronApi.worktree.create(
|
||||||
|
currentProject.path,
|
||||||
|
finalBranchName
|
||||||
|
);
|
||||||
|
if (result.success && result.worktree) {
|
||||||
|
logger.info(
|
||||||
|
`Worktree for branch "${finalBranchName}" ${
|
||||||
|
result.worktree?.isNew ? 'created' : 'already exists'
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
// Auto-select the worktree when creating/using it for bulk update
|
||||||
|
addAndSelectWorktree(result.worktree);
|
||||||
|
// Refresh worktree list in UI
|
||||||
|
setWorktreeRefreshKey((k) => k + 1);
|
||||||
|
} else if (!result.success) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to create worktree for branch "${finalBranchName}":`,
|
||||||
|
result.error
|
||||||
|
);
|
||||||
|
toast.error('Failed to create worktree', {
|
||||||
|
description: result.error || 'An error occurred',
|
||||||
|
});
|
||||||
|
return; // Don't proceed with update if worktree creation failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error creating worktree:', error);
|
||||||
|
toast.error('Failed to create worktree', {
|
||||||
|
description: error instanceof Error ? error.message : 'An error occurred',
|
||||||
|
});
|
||||||
|
return; // Don't proceed with update if worktree creation failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the final branch name in updates
|
||||||
|
const finalUpdates = {
|
||||||
|
...updates,
|
||||||
|
branchName: finalBranchName,
|
||||||
|
};
|
||||||
|
|
||||||
const api = getHttpApiClient();
|
const api = getHttpApiClient();
|
||||||
const featureIds = Array.from(selectedFeatureIds);
|
const featureIds = Array.from(selectedFeatureIds);
|
||||||
const result = await api.features.bulkUpdate(currentProject.path, featureIds, updates);
|
const result = await api.features.bulkUpdate(currentProject.path, featureIds, finalUpdates);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// Update local state
|
// Update local state
|
||||||
featureIds.forEach((featureId) => {
|
featureIds.forEach((featureId) => {
|
||||||
updateFeature(featureId, updates);
|
updateFeature(featureId, finalUpdates);
|
||||||
});
|
});
|
||||||
toast.success(`Updated ${result.updatedCount} features`);
|
toast.success(`Updated ${result.updatedCount} features`);
|
||||||
exitSelectionMode();
|
exitSelectionMode();
|
||||||
@@ -517,7 +589,16 @@ export function BoardView() {
|
|||||||
toast.error('Failed to update features');
|
toast.error('Failed to update features');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[currentProject, selectedFeatureIds, updateFeature, exitSelectionMode]
|
[
|
||||||
|
currentProject,
|
||||||
|
selectedFeatureIds,
|
||||||
|
updateFeature,
|
||||||
|
exitSelectionMode,
|
||||||
|
currentWorktreeBranch,
|
||||||
|
getPrimaryWorktreeBranch,
|
||||||
|
addAndSelectWorktree,
|
||||||
|
setWorktreeRefreshKey,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handler for bulk deleting multiple features
|
// Handler for bulk deleting multiple features
|
||||||
@@ -1325,6 +1406,9 @@ export function BoardView() {
|
|||||||
onClose={() => setShowMassEditDialog(false)}
|
onClose={() => setShowMassEditDialog(false)}
|
||||||
selectedFeatures={selectedFeatures}
|
selectedFeatures={selectedFeatures}
|
||||||
onApply={handleBulkUpdate}
|
onApply={handleBulkUpdate}
|
||||||
|
branchSuggestions={branchSuggestions}
|
||||||
|
branchCardCounts={branchCardCounts}
|
||||||
|
currentBranch={currentWorktreeBranch || undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Board Background Modal */}
|
{/* Board Background Modal */}
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import { Label } from '@/components/ui/label';
|
|||||||
import { AlertCircle } from 'lucide-react';
|
import { AlertCircle } from 'lucide-react';
|
||||||
import { modelSupportsThinking } from '@/lib/utils';
|
import { modelSupportsThinking } from '@/lib/utils';
|
||||||
import { Feature, ModelAlias, ThinkingLevel, PlanningMode } from '@/store/app-store';
|
import { Feature, ModelAlias, ThinkingLevel, PlanningMode } from '@/store/app-store';
|
||||||
import { TestingTabContent, PrioritySelect, PlanningModeSelect } from '../shared';
|
import { TestingTabContent, PrioritySelect, PlanningModeSelect, WorkModeSelector } from '../shared';
|
||||||
|
import type { WorkMode } from '../shared';
|
||||||
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
||||||
import { isCursorModel, isClaudeModel, type PhaseModelEntry } from '@automaker/types';
|
import { isCursorModel, isClaudeModel, type PhaseModelEntry } from '@automaker/types';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@@ -23,7 +24,10 @@ interface MassEditDialogProps {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
selectedFeatures: Feature[];
|
selectedFeatures: Feature[];
|
||||||
onApply: (updates: Partial<Feature>) => Promise<void>;
|
onApply: (updates: Partial<Feature>, workMode: WorkMode) => Promise<void>;
|
||||||
|
branchSuggestions: string[];
|
||||||
|
branchCardCounts?: Record<string, number>;
|
||||||
|
currentBranch?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ApplyState {
|
interface ApplyState {
|
||||||
@@ -33,6 +37,7 @@ interface ApplyState {
|
|||||||
requirePlanApproval: boolean;
|
requirePlanApproval: boolean;
|
||||||
priority: boolean;
|
priority: boolean;
|
||||||
skipTests: boolean;
|
skipTests: boolean;
|
||||||
|
branchName: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMixedValues(features: Feature[]): Record<string, boolean> {
|
function getMixedValues(features: Feature[]): Record<string, boolean> {
|
||||||
@@ -47,6 +52,7 @@ function getMixedValues(features: Feature[]): Record<string, boolean> {
|
|||||||
),
|
),
|
||||||
priority: !features.every((f) => f.priority === first.priority),
|
priority: !features.every((f) => f.priority === first.priority),
|
||||||
skipTests: !features.every((f) => f.skipTests === first.skipTests),
|
skipTests: !features.every((f) => f.skipTests === first.skipTests),
|
||||||
|
branchName: !features.every((f) => f.branchName === first.branchName),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +103,15 @@ function FieldWrapper({ label, isMixed, willApply, onApplyChange, children }: Fi
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: MassEditDialogProps) {
|
export function MassEditDialog({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
selectedFeatures,
|
||||||
|
onApply,
|
||||||
|
branchSuggestions,
|
||||||
|
branchCardCounts,
|
||||||
|
currentBranch,
|
||||||
|
}: MassEditDialogProps) {
|
||||||
const [isApplying, setIsApplying] = useState(false);
|
const [isApplying, setIsApplying] = useState(false);
|
||||||
|
|
||||||
// Track which fields to apply
|
// Track which fields to apply
|
||||||
@@ -108,6 +122,7 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
|
|||||||
requirePlanApproval: false,
|
requirePlanApproval: false,
|
||||||
priority: false,
|
priority: false,
|
||||||
skipTests: false,
|
skipTests: false,
|
||||||
|
branchName: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Field values
|
// Field values
|
||||||
@@ -118,6 +133,18 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
|
|||||||
const [priority, setPriority] = useState(2);
|
const [priority, setPriority] = useState(2);
|
||||||
const [skipTests, setSkipTests] = useState(false);
|
const [skipTests, setSkipTests] = useState(false);
|
||||||
|
|
||||||
|
// Work mode and branch name state
|
||||||
|
const [workMode, setWorkMode] = useState<WorkMode>(() => {
|
||||||
|
// Derive initial work mode from first selected feature's branchName
|
||||||
|
if (selectedFeatures.length > 0 && selectedFeatures[0].branchName) {
|
||||||
|
return 'custom';
|
||||||
|
}
|
||||||
|
return 'current';
|
||||||
|
});
|
||||||
|
const [branchName, setBranchName] = useState(() => {
|
||||||
|
return getInitialValue(selectedFeatures, 'branchName', '') as string;
|
||||||
|
});
|
||||||
|
|
||||||
// Calculate mixed values
|
// Calculate mixed values
|
||||||
const mixedValues = useMemo(() => getMixedValues(selectedFeatures), [selectedFeatures]);
|
const mixedValues = useMemo(() => getMixedValues(selectedFeatures), [selectedFeatures]);
|
||||||
|
|
||||||
@@ -131,6 +158,7 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
|
|||||||
requirePlanApproval: false,
|
requirePlanApproval: false,
|
||||||
priority: false,
|
priority: false,
|
||||||
skipTests: false,
|
skipTests: false,
|
||||||
|
branchName: false,
|
||||||
});
|
});
|
||||||
setModel(getInitialValue(selectedFeatures, 'model', 'sonnet') as ModelAlias);
|
setModel(getInitialValue(selectedFeatures, 'model', 'sonnet') as ModelAlias);
|
||||||
setThinkingLevel(getInitialValue(selectedFeatures, 'thinkingLevel', 'none') as ThinkingLevel);
|
setThinkingLevel(getInitialValue(selectedFeatures, 'thinkingLevel', 'none') as ThinkingLevel);
|
||||||
@@ -138,6 +166,10 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
|
|||||||
setRequirePlanApproval(getInitialValue(selectedFeatures, 'requirePlanApproval', false));
|
setRequirePlanApproval(getInitialValue(selectedFeatures, 'requirePlanApproval', false));
|
||||||
setPriority(getInitialValue(selectedFeatures, 'priority', 2));
|
setPriority(getInitialValue(selectedFeatures, 'priority', 2));
|
||||||
setSkipTests(getInitialValue(selectedFeatures, 'skipTests', false));
|
setSkipTests(getInitialValue(selectedFeatures, 'skipTests', false));
|
||||||
|
// Reset work mode and branch name
|
||||||
|
const initialBranchName = getInitialValue(selectedFeatures, 'branchName', '') as string;
|
||||||
|
setBranchName(initialBranchName);
|
||||||
|
setWorkMode(initialBranchName ? 'custom' : 'current');
|
||||||
}
|
}
|
||||||
}, [open, selectedFeatures]);
|
}, [open, selectedFeatures]);
|
||||||
|
|
||||||
@@ -150,6 +182,12 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
|
|||||||
if (applyState.requirePlanApproval) updates.requirePlanApproval = requirePlanApproval;
|
if (applyState.requirePlanApproval) updates.requirePlanApproval = requirePlanApproval;
|
||||||
if (applyState.priority) updates.priority = priority;
|
if (applyState.priority) updates.priority = priority;
|
||||||
if (applyState.skipTests) updates.skipTests = skipTests;
|
if (applyState.skipTests) updates.skipTests = skipTests;
|
||||||
|
if (applyState.branchName) {
|
||||||
|
// For 'current' mode, use empty string (work on current branch)
|
||||||
|
// For 'auto' mode, use empty string (will be auto-generated)
|
||||||
|
// For 'custom' mode, use the specified branch name
|
||||||
|
updates.branchName = workMode === 'custom' ? branchName : '';
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(updates).length === 0) {
|
if (Object.keys(updates).length === 0) {
|
||||||
onClose();
|
onClose();
|
||||||
@@ -158,7 +196,7 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
|
|||||||
|
|
||||||
setIsApplying(true);
|
setIsApplying(true);
|
||||||
try {
|
try {
|
||||||
await onApply(updates);
|
await onApply(updates, workMode);
|
||||||
onClose();
|
onClose();
|
||||||
} finally {
|
} finally {
|
||||||
setIsApplying(false);
|
setIsApplying(false);
|
||||||
@@ -293,6 +331,25 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
|
|||||||
testIdPrefix="mass-edit"
|
testIdPrefix="mass-edit"
|
||||||
/>
|
/>
|
||||||
</FieldWrapper>
|
</FieldWrapper>
|
||||||
|
|
||||||
|
{/* Branch / Work Mode */}
|
||||||
|
<FieldWrapper
|
||||||
|
label="Branch / Work Mode"
|
||||||
|
isMixed={mixedValues.branchName}
|
||||||
|
willApply={applyState.branchName}
|
||||||
|
onApplyChange={(apply) => setApplyState((prev) => ({ ...prev, branchName: apply }))}
|
||||||
|
>
|
||||||
|
<WorkModeSelector
|
||||||
|
workMode={workMode}
|
||||||
|
onWorkModeChange={setWorkMode}
|
||||||
|
branchName={branchName}
|
||||||
|
onBranchNameChange={setBranchName}
|
||||||
|
branchSuggestions={branchSuggestions}
|
||||||
|
branchCardCounts={branchCardCounts}
|
||||||
|
currentBranch={currentBranch}
|
||||||
|
testIdPrefix="mass-edit-work-mode"
|
||||||
|
/>
|
||||||
|
</FieldWrapper>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
|||||||
@@ -137,11 +137,10 @@ volumes:
|
|||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
| Problem | Solution |
|
| Problem | Solution |
|
||||||
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| Container won't start | Check `.env` has `ANTHROPIC_API_KEY` set. Run `docker-compose logs` for errors. |
|
| Container won't start | Check `.env` has `ANTHROPIC_API_KEY` set. Run `docker-compose logs` for errors. |
|
||||||
| Can't access web UI | Verify container is running with `docker ps \| grep automaker` |
|
| Can't access web UI | Verify container is running with `docker ps \| grep automaker` |
|
||||||
| Need a fresh start | Run `docker-compose down && docker volume rm automaker-data && docker-compose up -d --build` |
|
| Need a fresh start | Run `docker-compose down && docker volume rm automaker-data && docker-compose up -d --build` |
|
||||||
| Cursor auth fails | Re-extract token with `./scripts/get-cursor-token.sh` - tokens expire periodically. Make sure you've run `cursor-agent login` on your host first. |
|
| Cursor auth fails | Re-extract token with `./scripts/get-cursor-token.sh` - tokens expire periodically. Make sure you've run `cursor-agent login` on your host first. |
|
||||||
| OpenCode not detected | Mount `~/.local/share/opencode` to `/home/automaker/.local/share/opencode`. Make sure you've run `opencode auth login` on your host first. |
|
| OpenCode not detected | Mount `~/.local/share/opencode` to `/home/automaker/.local/share/opencode`. Make sure you've run `opencode auth login` on your host first. |
|
||||||
| File permission errors | Rebuild with `UID=$(id -u) GID=$(id -g) docker-compose build` to match container user to your host user. See [Fixing File Permission Issues](#fixing-file-permission-issues). |
|
| File permission errors | Rebuild with `UID=$(id -u) GID=$(id -g) docker-compose build` to match container user to your host user. See [Fixing File Permission Issues](#fixing-file-permission-issues). |
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user