{"record":{"id":"a6f66e2599858534","repo":"AlistGo/alist","slug":"cannot-copy-a-submodule","errorCode":null,"errorMessage":"cannot copy a submodule","messagePattern":"cannot copy a submodule","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"drivers/github/driver.go","lineNumber":903,"sourceCode":"func (d *Github) copyWithoutRenewTree(srcObj, dstDir model.Obj) (dstSha, newSha, srcParentSha string, srcParentTree *TreeResp, err error) {\n\tdst, err := d.get(dstDir.GetPath())\n\tif err != nil {\n\t\treturn \"\", \"\", \"\", nil, err\n\t}\n\tif dst.Entries == nil {\n\t\treturn \"\", \"\", \"\", nil, errs.NotFolder\n\t}\n\tdstSha = dst.Sha\n\tsrcParentPath := stdpath.Dir(srcObj.GetPath())\n\tsrcParentTree, srcParentSha, err = d.getTreeDirectly(srcParentPath)\n\tif err != nil {\n\t\treturn \"\", \"\", \"\", nil, err\n\t}\n\tvar src *TreeObjReq = nil\n\tfor _, t := range srcParentTree.Trees {\n\t\tif t.Path == srcObj.GetName() {\n\t\t\tif t.Type == \"commit\" {\n\t\t\t\treturn \"\", \"\", \"\", nil, errors.New(\"cannot copy a submodule\")\n\t\t\t}\n\t\t\tsrc = &t.TreeObjReq\n\t\t\tbreak\n\t\t}\n\t}\n\tif src == nil {\n\t\treturn \"\", \"\", \"\", nil, errs.ObjectNotFound\n\t}\n\n\tnewTree := make([]interface{}, 0, 2)\n\tnewTree = append(newTree, *src)\n\tif len(dst.Entries) == 1 && dst.Entries[0].Name == \".gitkeep\" {\n\t\tnewTree = append(newTree, TreeObjReq{\n\t\t\tPath: \".gitkeep\",\n\t\t\tMode: \"100644\",\n\t\t\tType: \"blob\",\n\t\t\tSha:  nil,\n\t\t})","sourceCodeStart":885,"sourceCodeEnd":921,"githubUrl":"https://github.com/AlistGo/alist/blob/843d9dc8149126976b2625911e45a4d3ffd6f2f5/drivers/github/driver.go#L885-L921","documentation":"In the copy helper used by Copy/Move, after fetching the source parent tree the driver scans for the source entry; an entry of Type == \"commit\" is a git submodule (gitlink). Git submodule pointers reference commits in foreign repositories and cannot be duplicated into an arbitrary destination tree by this driver, so the copy is rejected.","triggerScenarios":"Calling Copy or Move where srcObj is a submodule directory — the tree walk hits t.Type == \"commit\" for the matching path and returns before any tree mutation.","commonSituations":"Trying to duplicate or relocate a vendored submodule (themes, SDKs) into another folder of the same repo via the OpenList UI.","solutions":["Clone locally and use git: move the submodule path, update .gitmodules, commit, push","Copy the submodule's upstream URL and re-add it at the destination with git submodule add","Replace the submodule with plain vendored files if it must be copyable through the driver"],"exampleFix":"# before: Copy submodule-folder -> error\n# after: in a local clone\ngit submodule deinit -f <old-path> && git rm <old-path>\ngit submodule add <url> <new-path>\ngit commit -m \"move submodule\" && git push","handlingStrategy":"validation","validationCode":"// Before Copy/Move, verify the source is not a submodule\ntree, _, err := d.getTreeDirectly(stdpath.Dir(src.GetPath()))\nif err != nil { return err }\nfor _, t := range tree.Trees {\n\tif t.Path == src.GetName() && t.Type == \"commit\" {\n\t\treturn errors.New(\"source is a submodule; copy it with git instead\")\n\t}\n}\nreturn d.Copy(ctx, src, dst)","typeGuard":"func isSubmoduleEntry(t TreeEntry) bool { return t.Type == \"commit\" }","tryCatchPattern":"if err := d.Copy(ctx, src, dst); err != nil {\n\tif strings.Contains(err.Error(), \"cannot copy a submodule\") {\n\t\treturn fmt.Errorf(\"%s is a submodule and must be copied via git\", src.GetPath())\n\t}\n\treturn err\n}","preventionTips":["Mark submodule directories in listings so copy/move UI can disable them","Script submodule relocation with git submodule add/deinit","Keep .gitmodules in review checklists so submodules are never copied accidentally"],"tags":["github","git","submodule","copy","openlist"],"backgroundTag":null,"analyzedSha":"843d9dc8149126976b2625911e45a4d3ffd6f2f5","analyzedAt":"2026-08-15T12:14:11.722Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}