{"record":{"id":"f6cf72bc9810eadc","repo":"AlistGo/alist","slug":"cannot-remove-a-submodule","errorCode":null,"errorMessage":"cannot remove a submodule","messagePattern":"cannot remove a submodule","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"drivers/github/driver.go","lineNumber":572,"sourceCode":"\treturn d.commit(message, rootSha)\n}\n\nfunc (d *Github) Remove(ctx context.Context, obj model.Obj) error {\n\tif !d.isOnBranch {\n\t\treturn errors.New(\"cannot write to non-branch reference\")\n\t}\n\td.commitMutex.Lock()\n\tdefer d.commitMutex.Unlock()\n\tparentDir := stdpath.Dir(obj.GetPath())\n\ttree, treeSha, err := d.getTreeDirectly(parentDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar del *TreeObjReq = nil\n\tfor _, t := range tree.Trees {\n\t\tif t.Path == obj.GetName() {\n\t\t\tif t.Type == \"commit\" {\n\t\t\t\treturn errors.New(\"cannot remove a submodule\")\n\t\t\t}\n\t\t\tdel = &t.TreeObjReq\n\t\t\tdel.Sha = nil\n\t\t\tbreak\n\t\t}\n\t}\n\tif del == nil {\n\t\treturn errs.ObjectNotFound\n\t}\n\tnewTree := make([]interface{}, 0, 2)\n\tnewTree = append(newTree, *del)\n\tif len(tree.Trees) == 1 { // completely emptying the repository will get a 404\n\t\tnewTree = append(newTree, map[string]string{\n\t\t\t\"path\":    \".gitkeep\",\n\t\t\t\"mode\":    \"100644\",\n\t\t\t\"type\":    \"blob\",\n\t\t\t\"content\": \"\",\n\t\t})","sourceCodeStart":554,"sourceCodeEnd":590,"githubUrl":"https://github.com/AlistGo/alist/blob/843d9dc8149126976b2625911e45a4d3ffd6f2f5/drivers/github/driver.go#L554-L590","documentation":"During Remove the driver fetches the parent git tree and looks for the entry matching the object's name. A tree entry with Type == \"commit\" is a git submodule pointer, not a regular blob or tree. The driver cannot compose a valid delete commit for a submodule link, so it explicitly rejects the operation with this error.","triggerScenarios":"Calling Remove on a directory or file whose corresponding entry in the GitHub tree has type 'commit' — i.e. the path is a submodule defined in .gitmodules; the loop in Remove finds t.Type == \"commit\" and bails out before building the new tree.","commonSituations":"A repo vendors dependencies as submodules (common in C/C++ projects, dotfiles frameworks, themes) and the user tries to delete the submodule folder through the OpenList file manager.","solutions":["Remove the submodule with plain git: git rm <path>, delete the .gitmodules entry, commit and push","Edit .gitmodules and the gitlink entry manually via a commit, then let OpenList re-list","If submodules are not wanted, replace them with vendored copies or a package manager"],"exampleFix":"# before: delete submodule via OpenList UI -> error\n# after: in a local clone\ngit rm --cached <submodule-path>\ngit rm <submodule-path>\ngit commit -m \"remove submodule\"\ngit push","handlingStrategy":"validation","validationCode":"// Check tree entry type before calling Remove\ntree, _, _ := d.getTreeDirectly(stdpath.Dir(obj.GetPath()))\nfor _, t := range tree.Trees {\n\tif t.Path == obj.GetName() && t.Type == \"commit\" {\n\t\treturn errors.New(\"path is a submodule; remove it with git\")\n\t}\n}\nreturn d.Remove(ctx, obj)","typeGuard":"func isSubmoduleEntry(t TreeEntry) bool { return t.Type == \"commit\" }","tryCatchPattern":"if err := d.Remove(ctx, obj); err != nil {\n\tif strings.Contains(err.Error(), \"cannot remove a submodule\") {\n\t\tlog.Warnf(\"%s is a submodule; removing via git instead\", obj.GetPath())\n\t\treturn nil // handled out-of-band\n\t}\n\treturn err\n}","preventionTips":["List submodules (git submodule status) and treat them as opaque in UI flows","Filter Type==commit entries out of user-deletable listings","Automate submodule removal in CI with git rm, not through the file API"],"tags":["github","git","submodule","driver","openlist"],"backgroundTag":null,"analyzedSha":"843d9dc8149126976b2625911e45a4d3ffd6f2f5","analyzedAt":"2026-08-15T12:14:11.722Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}