/
githubmirror
/
lazygit
Обзор
Документация
Войти
/
githubmirror
/
lazygit
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
pkg/commands/git_commands/submodule.go
370 строк
12 KB
Stefan Haller
Don't let our repo answer for a different one
08 авг 2026, 12:15
08 авг 2026, 12:15
34d41b5
Код
Авторство
О чём код?
package git_commands import ( "bufio" "os" "path/filepath" "regexp" "strings" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/samber/lo" ) // .gitmodules looks like this: // [submodule "mysubmodule"] // path = blah/mysubmodule // url = git@github.com:subbo.git type SubmoduleCommands struct { *GitCommon } func NewSubmoduleCommands(gitCommon *GitCommon) *SubmoduleCommands { return &SubmoduleCommands{ GitCommon: gitCommon, } } func (self *SubmoduleCommands) GetConfigs(parentModule *models.SubmoduleConfig) ([]*models.SubmoduleConfig, error) { // Resolve the path against the repo this commands object was created for // rather than the process working directory, so that a read from a // still-running refresh keeps addressing that repo after the user // switched to another one. dir := self.repoPaths.WorktreePath() if parentModule != nil { dir = filepath.Join(dir, parentModule.FullPath()) } gitModulesPath := filepath.Join(dir, ".gitmodules") file, err := os.Open(gitModulesPath) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } defer file.Close() scanner := bufio.NewScanner(file) scanner.Split(bufio.ScanLines) firstMatch := func(str string, regex string) (string, bool) { re := regexp.MustCompile(regex) matches := re.FindStringSubmatch(str) if len(matches) > 0 { return matches[1], true } return "", false } configs := []*models.SubmoduleConfig{} lastConfigIdx := -1 for scanner.Scan() { line := scanner.Text() if name, ok := firstMatch(line, `\[submodule "(.*)"\]`); ok { configs = append(configs, &models.SubmoduleConfig{ Name: name, ParentModule: parentModule, }) lastConfigIdx = len(configs) - 1 continue } if lastConfigIdx != -1 { if path, ok := firstMatch(line, `\s*path\s*=\s*(.*)\s*`); ok { configs[lastConfigIdx].Path = path nestedConfigs, err := self.GetConfigs(configs[lastConfigIdx]) if err == nil { configs = append(configs, nestedConfigs...) } } else if url, ok := firstMatch(line, `\s*url\s*=\s*(.*)\s*`); ok { configs[lastConfigIdx].Url = url } } } if err := scanner.Err(); err != nil { return nil, err } return configs, nil } // AnyHaveStageableChanges reports whether any of the given submodule paths has // a checked-out commit that differs from the one recorded in the // superproject's index, i.e. a change that `git add <path>` would actually // stage. A submodule that only has dirty or untracked content (with no new // commit) can't be staged from the superproject, so it won't be reported here. func (self *SubmoduleCommands) AnyHaveStageableChanges(paths []string) (bool, error) { if len(paths) == 0 { return false, nil } cmdArgs := NewGitCmd("submodule").Arg("status", "--").Arg(paths...).ToArgv() output, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput() if err != nil { return false, err } // Each line looks like "<prefix><sha> <path> (<describe>)". A '+' prefix // means the checked-out commit differs from the index, i.e. there's a // commit change to stage. return lo.SomeBy(strings.Split(output, "\n"), func(line string) bool { return strings.HasPrefix(line, "+") }), nil } // GetConflictCommits returns the three gitlink commits of a conflicted submodule // from the index: the merge base, our (current) commit, and their (incoming) // commit. Any of them can be empty if that stage is absent (e.g. a submodule // that was added on only one side). The path is relative to the repo root. func (self *SubmoduleCommands) GetConflictCommits(path string) (base string, ours string, theirs string, err error) { cmdArgs := NewGitCmd("ls-files").Arg("-u", "-z", "--", path).ToArgv() output, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput() if err != nil { return "", "", "", err } // Each NUL-terminated entry looks like "<mode> <sha> <stage>\t<path>". for _, entry := range strings.Split(output, "\x00") { // fields are split on the tab and the spaces, so the leading three are // always mode, sha, stage regardless of what the path contains. fields := strings.Fields(entry) if len(fields) < 3 { continue } switch fields[2] { case "1": base = fields[1] case "2": ours = fields[1] case "3": theirs = fields[1] } } return base, ours, theirs, nil } // GetCommitSummary returns "<short-sha> <subject>" for a commit inside the // submodule at the given path, for display in the conflict menu. func (self *SubmoduleCommands) GetCommitSummary(path string, sha string) (string, error) { cmdArgs := NewGitCmd("log"). Dir(path). Arg("--format=%h %s", "--max-count=1", sha). Config("log.showsignature=false"). ToArgv() summary, err := forOtherRepo(self.cmd.New(cmdArgs)).DontLog().RunWithOutput() return strings.TrimSpace(summary), err } // CheckoutConflictCommit resolves a submodule conflict by checking the submodule // out at the given commit. `git checkout --ours/--theirs` is a no-op on // gitlinks, so we check out the chosen commit in the submodule itself; the // caller then stages the submodule to record the resolution. func (self *SubmoduleCommands) CheckoutConflictCommit(path string, sha string) error { cmdArgs := NewGitCmd("checkout").Dir(path).Arg(sha).ToArgv() return forOtherRepo(self.cmd.New(cmdArgs)).Run() } // ConflictSideLog returns a oneline log, run inside the submodule, of the commits // that `side` has but `otherSide` does not (i.e. `otherSide..side`) — the commits // unique to one side of a commit conflict, relative to their common ancestor. It // is empty if `side` is an ancestor of `otherSide` (e.g. that side was rewound). func (self *SubmoduleCommands) ConflictSideLog(path string, side string, otherSide string) (string, error) { cmdArgs := NewGitCmd("log").Dir(path). Arg("--oneline", "--color=always", otherSide+".."+side). ToArgv() return forOtherRepo(self.cmd.New(cmdArgs)).DontLog().RunWithOutput() } func (self *SubmoduleCommands) Stash(submodule *models.SubmoduleConfig) error { // if the path does not exist then it hasn't yet been initialized so we'll swallow the error // because the intention here is to have no dirty worktree state if _, err := os.Stat(filepath.Join(self.repoPaths.WorktreePath(), submodule.FullPath())); os.IsNotExist(err) { self.Log.Infof("submodule path %s does not exist, returning", submodule.FullPath()) return nil } cmdArgs := NewGitCmd("stash"). Dir(submodule.FullPath()). Arg("--include-untracked"). ToArgv() return forOtherRepo(self.cmd.New(cmdArgs)).Run() } func (self *SubmoduleCommands) Reset(submodule *models.SubmoduleConfig) error { cmdArgs := NewGitCmd("submodule"). Arg("update", "--init", "--force", "--", submodule.Path). ToArgv() return self.runInParentModule(submodule, self.cmd.New(cmdArgs)) } func (self *SubmoduleCommands) UpdateAll() error { // not doing an --init here because the user probably doesn't want that cmdArgs := NewGitCmd("submodule").Arg("update", "--force").ToArgv() return self.cmd.New(cmdArgs).Run() } // runInParentModule runs the given command in the submodule's parent module's // directory when the submodule is nested: its path arguments (and the // .gitmodules file the config commands touch) are relative to the parent // module. The directory is set on the command itself rather than by // temporarily chdir-ing the process there, which would leak the parent // module's directory into whatever other commands run concurrently (e.g. a // background refresh's). // // That directory is relative, so it resolves against the process working // directory rather than against the repo directory the command builder // otherwise pins commands to. Only foreground commands the user issued end up // here, and lazygit won't switch repos while one of those is in flight, so the // two are the same directory; don't call this from background work, where they // need not be. func (self *SubmoduleCommands) runInParentModule(submodule *models.SubmoduleConfig, cmdObj *oscommands.CmdObj) error { if submodule.ParentModule != nil { forOtherRepo(cmdObj.SetWd(submodule.ParentModule.FullPath())) } return cmdObj.Run() } func (self *SubmoduleCommands) Delete(submodule *models.SubmoduleConfig) error { // based on https://gist.github.com/myusuf3/7f645819ded92bda6677 if err := self.runInParentModule(submodule, self.cmd.New( NewGitCmd("submodule"). Arg("deinit", "--force", "--", submodule.Path).ToArgv(), )); err != nil { if !strings.Contains(err.Error(), "did not match any file(s) known to git") { return err } if err := self.runInParentModule(submodule, self.cmd.New( NewGitCmd("config"). Arg("--file", ".gitmodules", "--remove-section", "submodule."+submodule.Path). ToArgv(), )); err != nil { return err } if err := self.runInParentModule(submodule, self.cmd.New( NewGitCmd("config"). Arg("--remove-section", "submodule."+submodule.Path). ToArgv(), )); err != nil { return err } } if err := self.runInParentModule(submodule, self.cmd.New( NewGitCmd("rm").Arg("--force", "-r", submodule.Path).ToArgv(), )); err != nil { // if the directory isn't there then that's fine self.Log.Error(err) } // We may in fact want to use the repo's git dir path but git docs say not to // mix submodules and worktrees anyway. return os.RemoveAll(submodule.GitDirPath(self.repoPaths.repoGitDirPath)) } func (self *SubmoduleCommands) Add(name string, path string, url string) error { cmdArgs := NewGitCmd("submodule"). Arg("add"). Arg("--force"). Arg("--name"). Arg(name). Arg("--"). Arg(url). Arg(path). ToArgv() return self.cmd.New(cmdArgs).Run() } func (self *SubmoduleCommands) UpdateUrl(submodule *models.SubmoduleConfig, newUrl string) error { setUrlCmdStr := NewGitCmd("config"). Arg( "--file", ".gitmodules", "submodule."+submodule.Name+".url", newUrl, ). ToArgv() // the set-url command is only for later git versions so we're doing it manually here if err := self.runInParentModule(submodule, self.cmd.New(setUrlCmdStr)); err != nil { return err } syncCmdStr := NewGitCmd("submodule").Arg("sync", "--", submodule.Path). ToArgv() if err := self.runInParentModule(submodule, self.cmd.New(syncCmdStr)); err != nil { return err } return nil } func (self *SubmoduleCommands) Init(path string) error { cmdArgs := NewGitCmd("submodule").Arg("init", "--", path). ToArgv() return self.cmd.New(cmdArgs).Run() } func (self *SubmoduleCommands) Update(path string) error { cmdArgs := NewGitCmd("submodule").Arg("update", "--init", "--", path). ToArgv() return self.cmd.New(cmdArgs).Run() } func (self *SubmoduleCommands) BulkInitCmdObj() *oscommands.CmdObj { cmdArgs := NewGitCmd("submodule").Arg("init"). ToArgv() return self.cmd.New(cmdArgs) } func (self *SubmoduleCommands) BulkUpdateCmdObj() *oscommands.CmdObj { cmdArgs := NewGitCmd("submodule").Arg("update"). ToArgv() return self.cmd.New(cmdArgs) } func (self *SubmoduleCommands) ForceBulkUpdateCmdObj() *oscommands.CmdObj { cmdArgs := NewGitCmd("submodule").Arg("update", "--force"). ToArgv() return self.cmd.New(cmdArgs) } func (self *SubmoduleCommands) BulkUpdateRecursivelyCmdObj() *oscommands.CmdObj { cmdArgs := NewGitCmd("submodule").Arg("update", "--init", "--recursive"). ToArgv() return self.cmd.New(cmdArgs) } func (self *SubmoduleCommands) BulkDeinitCmdObj() *oscommands.CmdObj { cmdArgs := NewGitCmd("submodule").Arg("deinit", "--all", "--force"). ToArgv() return self.cmd.New(cmdArgs) } func (self *SubmoduleCommands) ResetSubmodules(submodules []*models.SubmoduleConfig) error { for _, submodule := range submodules { if err := self.Stash(submodule); err != nil { return err } } return self.UpdateAll() }