# How LazyGit Handles Submodule Operations and Display

> Discover how LazyGit manages Git submodules with its layered architecture, hierarchical models, and intuitive display. Perform full CRUD operations easily.

- Repository: [Jesse Duffield/lazygit](https://github.com/jesseduffield/lazygit)
- Tags: internals
- Published: 2026-03-02

---

**LazyGit treats Git submodules as first-class objects using a layered architecture that parses `.gitmodules` into hierarchical models, renders them as indented trees in the side panel, and exposes full CRUD operations through the `SubmodulesController` with bulk command support.**

LazyGit provides comprehensive submodule management through a clean separation between data models, Git command wrappers, and UI presentation. According to the jesseduffield/lazygit source code, the implementation spans five key components that handle everything from parsing nested submodule configurations to executing bulk updates across your repository hierarchy.

## Architecture Overview

The lazygit submodule operations and display system follows a strict four-layer pattern. The **Model** layer defines the data structures, the **Commands** layer wraps Git operations, the **Presentation** layer handles visual rendering, and the **Context** layer manages UI state and view binding.

At startup, `SubmoduleCommands.GetConfigs` reads `.gitmodules` recursively to populate `c.Model().Submodules`. The `SubmodulesContext` binds this data to the side panel view, while `SubmodulesController` wires user keybindings to command invocations.

## Data Model and Configuration Parsing

### The SubmoduleConfig Structure

In [`pkg/commands/models/submodule_config.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/commands/models/submodule_config.go), the `SubmoduleConfig` struct stores submodule metadata including `Name`, `Path`, `Url`, and a pointer to its `Parent` submodule. This design supports arbitrarily nested hierarchies.

Helper methods `FullName()` and `FullPath()` recursively prepend parent information to generate fully qualified identifiers:

```go
type SubmoduleConfig struct {
    Name   string
    Path   string
    Url    string
    Parent *SubmoduleConfig
}

func (s *SubmoduleConfig) FullName() string {
    if s.Parent != nil {
        return s.Parent.FullName() + "/" + s.Name
    }
    return s.Name
}

```

### Parsing .gitmodules Recursively

The `SubmoduleCommands` struct in [`pkg/commands/git_commands/submodule.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/commands/git_commands/submodule.go) discovers submodules via the `GetConfigs` method. This function parses `.gitmodules` using `git config` commands and recursively walks nested modules by checking for submodule configurations within parent paths. The resulting slice is stored in the global application model at `c.Model().Submodules`.

## UI Presentation and Display

### Rendering the Submodule List

In [`pkg/gui/presentation/submodules.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/presentation/submodules.go), the `GetSubmoduleListDisplayStrings` function transforms the flat model slice into a hierarchical visual tree. It calculates indentation by traversing the parent chain and prefixes child entries with "‑":

```go
func GetSubmoduleListDisplayStrings(submodules []*models.SubmoduleConfig) [][]string {
    lines := make([][]string, 0, len(submodules))
    for _, sub := range submodules {
        // Calculate depth by traversing parent chain
        depth := 0
        parent := sub.Parent
        for parent != nil {
            depth++
            parent = parent.Parent
        }
        prefix := strings.Repeat("  ", depth) + "- "
        lines = append(lines, []string{prefix + sub.Name})
    }
    return lines
}

```

This produces an indented tree view in the side panel that reflects the nested structure of your `.gitmodules` configuration.

### Detail Panel Rendering

The `SubmodulesController` in [`pkg/gui/controllers/submodules_controller.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/controllers/submodules_controller.go) implements `GetOnRenderToMain` to populate the main view when a submodule is selected. It displays color-coded metadata and, when available, a working tree diff:

```go
func (self *SubmodulesController) GetOnRenderToMain() func() {
    return func() {
        sub := self.GetSelected()
        prefix := fmt.Sprintf(
            "Name: %s\nPath: %s\nUrl:  %s\n\n",
            style.FgGreen.Sprint(sub.FullName()),
            style.FgYellow.Sprint(sub.FullPath()),
            style.FgCyan.Sprint(sub.Url),
        )
        if file := self.c.Helpers().WorkingTree.FileForSubmodule(sub); file != nil {
            diffCmd := self.c.Helpers().WorkingTree.WorktreeFileDiffCmdObj(file, false, !file.HasUnstagedChanges && file.HasStagedChanges)
            task := types.NewRunCommandTaskWithPrefix(diffCmd.GetCmd(), prefix)
            self.c.RenderToMainViews(task)
        }
    }
}

```

## User Operations and Keybindings

The `SubmodulesController` registers keybindings for all lazygit submodule operations. Each action runs inside `WithWaitingStatus` to display a spinner and logs the action for transparency.

### Individual Submodule Actions

- **Enter** (`goInto`): Opens the submodule repository in a new view via `Helpers().Repos.EnterSubmodule`
- **Add** (`new`): Prompts for URL, name, and path, then executes `c.Git().Submodule.Add`
- **Edit URL** (`edit`): Prompts for a new URL and calls `c.Git().Submodule.UpdateUrl`
- **Init** (`submodules.Init`): Runs `git submodule init` via `c.Git().Submodule.Init`
- **Update** (`submodules.Update`): Runs `git submodule update` via `c.Git().Submodule.Update`
- **Remove** (`remove`): Confirms deletion then runs `c.Git().Submodule.Delete`

Adding a submodule follows this pattern:

```go
// Prompt for URL → name → path, then invoke the git command
err := c.Git().Submodule.Add(name, path, url)
if err != nil { return err }
// Refresh UI
c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})

```

The `editURL` implementation demonstrates the standard pattern for submodule modifications:

```go
func (self *SubmodulesController) editURL(sub *models.SubmoduleConfig) error {
    return self.c.WithWaitingStatus(self.c.Tr.UpdatingSubmoduleUrlStatus, func(gocui.Task) error {
        self.c.LogAction(self.c.Tr.Actions.UpdateSubmoduleUrl)
        return self.c.Git().Submodule.UpdateUrl(sub, newUrl)
    })
}

```

### Bulk Operations

For repository-wide changes, `SubmoduleCommands` provides pre-built command objects that the controller executes via `openBulkActionsMenu`. These include:

- `BulkInitCmdObj`: Returns a `CmdObj` running `git submodule init`
- `BulkUpdateCmdObj`: Returns a `CmdObj` running `git submodule update`
- `BulkUpdateRecursivelyCmdObj`: Returns a `CmdObj` running recursive update
- `BulkDeinitCmdObj`: Returns a `CmdObj` running `git submodule deinit`

Example usage for bulk initialization:

```go
cmdObj := c.Git().Submodule.BulkInitCmdObj()
err := cmdObj.Run()
c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})

```

## Context and View Management

The `SubmodulesContext` in [`pkg/gui/context/submodules_context.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/context/submodules_context.go) connects the model layer to the UI. It wraps the `c.Model().Submodules` slice in a filtered list view bound to the "Submodules" side panel, enabling search and navigation:

```go
type SubmodulesContext struct {
    *FilteredListViewModel
    *ContextCommon
}

func NewSubmodulesContext(c *ContextCommon) *SubmodulesContext {
    return &SubmodulesContext{
        FilteredListViewModel: NewFilteredListViewModel(c.Model().Submodules),
        ContextCommon:         c,
    }
}

```

## Summary

- LazyGit implements submodules through a Model-Command-Presentation-Context architecture
- `SubmoduleConfig` models support nested hierarchies via parent references and `FullName()`/`FullPath()` helpers
- `SubmoduleCommands.GetConfigs` recursively parses `.gitmodules` to discover submodule trees
- The UI renders hierarchical lists with indentation in `GetSubmoduleListDisplayStrings` and detail panels with color-coded metadata
- Full CRUD operations are available via `SubmodulesController`, each wrapped with waiting status indicators
- Bulk operations use pre-constructed `CmdObj` instances (`BulkInitCmdObj`, `BulkUpdateCmdObj`, etc.) for efficient batch execution

## Frequently Asked Questions

### How does LazyGit display nested submodules hierarchically?

LazyGit calculates indentation levels by traversing parent chains in `GetSubmoduleListDisplayStrings`, prefixing each level with two spaces and a "‑" character. This creates a visual tree structure that mirrors the nested configuration in your `.gitmodules` files.

### Where does LazyGit store submodule configuration data?

The `SubmoduleConfig` structs are stored in the global application model at `c.Model().Submodules`, populated by `SubmoduleCommands.GetConfigs` reading from `.gitmodules` files recursively and linking child modules to their parents.

### Can I perform bulk operations on all submodules at once?

Yes. The `SubmodulesController` provides a bulk menu that executes `BulkInitCmdObj`, `BulkUpdateCmdObj`, `BulkUpdateRecursivelyCmdObj`, or `BulkDeinitCmdObj` to run Git commands across all configured submodules simultaneously, followed by a UI refresh.

### How does LazyGit show submodule status differences in the UI?

The `GetOnRenderToMain` method retrieves the working tree file via `FileForSubmodule` and generates a diff using `WorktreeFileDiffCmdObj`, displaying it alongside the submodule's metadata (name, path, URL) in the main view with color-coded formatting.