Skip to main content
GitHub Desktop uses a custom store-based architecture for state management, centered around a large central store coordinating multiple specialized stores.

Store Architecture

Location: app/src/lib/stores/ The store layer manages all application state and coordinates data flow between the UI and business logic.

AppStore

Central store coordinating all application state (~8,700 lines)

GitStore

Per-repository Git state and operations

RepositoriesStore

Repository list and metadata persistence

AccountsStore

User accounts and authentication tokens

State Interfaces

IAppState

File: app/src/lib/app-state.ts The central state interface defining all application state:
export interface IAppState {
  // User accounts
  readonly accounts: ReadonlyArray<Account>
  
  // Repository management
  readonly repositories: ReadonlyArray<Repository | CloningRepository>
  readonly recentRepositories: ReadonlyArray<number>
  readonly selectedState: PossibleSelections | null
  
  // UI state
  readonly windowState: WindowState
  readonly windowZoomFactor: number
  readonly currentPopup: Popup | null
  readonly currentBanner: Banner | null
  readonly currentFoldout: Foldout | null
  
  // Sign-in flow
  readonly signInState: SignInState | null
  
  // Theme
  readonly selectedTheme: ApplicationTheme
  
  // Repository-specific state cache
  readonly localRepositoryStateLookup: Map<number, ILocalRepositoryState>
}

export type PossibleSelections =
  | {
      type: SelectionType.Repository
      repository: Repository
      state: IRepositoryState
    }
  | {
      type: SelectionType.CloningRepository
      repository: CloningRepository
      progress: ICloneProgress
    }
  | { type: SelectionType.MissingRepository; repository: Repository }

IRepositoryState

File: app/src/lib/app-state.ts State for a single repository:
export interface IRepositoryState {
  // Git state
  readonly branchesState: IBranchesState
  readonly commitLookup: Map<string, Commit>
  readonly localCommitSHAs: ReadonlyArray<string>
  readonly aheadBehind: IAheadBehind | null
  readonly remote: IRemote | null
  
  // Working directory
  readonly workingDirectory: WorkingDirectoryStatus
  readonly commitMessage: ICommitMessage
  
  // History
  readonly commitSelection: CommitSelection
  readonly selectedSection: RepositorySectionTab
  
  // Pull requests
  readonly currentPullRequest: PullRequest | null
  readonly pullRequestSuggestedNextAction: PullRequestSuggestedNextAction | null
  
  // Multi-commit operations (rebase, cherry-pick)
  readonly multiCommitOperationState: IMultiCommitOperationState | null
  
  // Conflicts
  readonly conflictState: ConflictState | null
}

export interface IBranchesState {
  readonly tip: Tip
  readonly currentBranch: Branch | null
  readonly defaultBranch: Branch | null
  readonly allBranches: ReadonlyArray<Branch>
  readonly recentBranches: ReadonlyArray<Branch>
}
State interfaces are deeply readonly to prevent accidental mutations. New state is created via immutable updates.

AppStore

File: app/src/lib/stores/app-store.ts (8,717 lines) The central store orchestrating all application state:
export class AppStore extends BaseStore {
  private readonly repositories: RepositoriesStore
  private readonly accounts: AccountsStore
  private readonly gitStores = new Map<number, GitStore>()
  private readonly pullRequestCoordinator: PullRequestCoordinator
  
  private state: IAppState = {
    accounts: [],
    repositories: [],
    recentRepositories: [],
    selectedState: null,
    windowState: WindowState.Normal,
    windowZoomFactor: 1,
    currentPopup: null,
    currentBanner: null,
    currentFoldout: null,
    signInState: null,
    selectedTheme: ApplicationTheme.System,
    localRepositoryStateLookup: new Map(),
  }
  
  public constructor(
    private readonly dispatcher: Dispatcher,
    private readonly statsStore: StatsStore
  ) {
    super()
    
    this.repositories = new RepositoriesStore()
    this.accounts = new AccountsStore()
    this.pullRequestCoordinator = new PullRequestCoordinator(this)
  }
  
  public getState(): IAppState {
    return this.state
  }
  
  private emitUpdate() {
    this.emitter.emit('did-update', this.state)
  }
}

State Updates

State updates follow an immutable pattern:
export class AppStore extends BaseStore {
  // Update state immutably
  private updateState(update: Partial<IAppState>) {
    this.state = { ...this.state, ...update }
    this.emitUpdate()
  }
  
  // Update repository-specific state
  private updateRepositoryState(
    repository: Repository,
    update: Partial<IRepositoryState>
  ) {
    const currentState = this.getRepositoryState(repository)
    const newState = { ...currentState, ...update }
    
    this.state = {
      ...this.state,
      localRepositoryStateLookup: new Map(
        this.state.localRepositoryStateLookup
      ).set(repository.id, newState),
    }
    
    this.emitUpdate()
  }
}

Repository Operations

export class AppStore extends BaseStore {
  // Select a repository
  public async selectRepository(repository: Repository): Promise<void> {
    // Get or create GitStore for this repository
    const gitStore = this.getOrCreateGitStore(repository)
    
    // Load repository state
    const state = await this.loadRepositoryState(repository, gitStore)
    
    // Update selected state
    this.updateState({
      selectedState: {
        type: SelectionType.Repository,
        repository,
        state,
      },
    })
    
    // Start background refresh
    this.refreshRepository(repository)
  }
  
  // Refresh repository data
  private async refreshRepository(repository: Repository): Promise<void> {
    const gitStore = this.getGitStore(repository)
    
    // Fetch latest data in parallel
    await Promise.all([
      gitStore.loadStatus(),
      gitStore.loadBranches(),
      gitStore.loadAheadBehind(),
      this.pullRequestCoordinator.refresh(repository),
    ])
  }
}

GitStore

File: app/src/lib/stores/git-store.ts (1,400+ lines) Manages Git state for a single repository:
export class GitStore {
  private readonly repository: Repository
  private readonly operationQueue: OperationQueue
  
  private tip: Tip = { kind: TipState.Unknown }
  private branches: ReadonlyArray<Branch> = []
  private status: WorkingDirectoryStatus | null = null
  
  public constructor(repository: Repository) {
    this.repository = repository
    this.operationQueue = new OperationQueue()
  }
  
  // Load working directory status
  public async loadStatus(): Promise<WorkingDirectoryStatus> {
    const status = await getStatus(this.repository)
    this.status = status
    this.emitUpdate()
    return status
  }
  
  // Load branches
  public async loadBranches(): Promise<ReadonlyArray<Branch>> {
    const branches = await getBranches(this.repository)
    this.branches = branches
    this.emitUpdate()
    return branches
  }
  
  // Get current branch
  public getCurrentBranch(): Branch | null {
    if (this.tip.kind === TipState.Valid) {
      return this.tip.branch
    }
    return null
  }
}

Operation Queue

Serializes Git operations to prevent conflicts:
export class OperationQueue {
  private queue: Array<() => Promise<any>> = []
  private running = false
  
  public async enqueue<T>(operation: () => Promise<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      this.queue.push(async () => {
        try {
          const result = await operation()
          resolve(result)
        } catch (error) {
          reject(error)
        }
      })
      
      this.processQueue()
    })
  }
  
  private async processQueue() {
    if (this.running || this.queue.length === 0) {
      return
    }
    
    this.running = true
    
    while (this.queue.length > 0) {
      const operation = this.queue.shift()!
      await operation()
    }
    
    this.running = false
  }
}
The operation queue ensures Git operations execute serially, preventing race conditions and repository corruption.

Specialized Stores

RepositoriesStore

File: app/src/lib/stores/repositories-store.ts Persists repository list to local database:
export class RepositoriesStore {
  private readonly db: RepositoriesDatabase
  private repositories: ReadonlyArray<Repository> = []
  
  public async loadRepositories(): Promise<ReadonlyArray<Repository>> {
    this.repositories = await this.db.repositories.toArray()
    return this.repositories
  }
  
  public async addRepository(path: string): Promise<Repository> {
    const repository = new Repository(path, Date.now())
    await this.db.repositories.add(repository)
    this.repositories = [...this.repositories, repository]
    this.emitUpdate()
    return repository
  }
  
  public async removeRepository(repository: Repository): Promise<void> {
    await this.db.repositories.delete(repository.id)
    this.repositories = this.repositories.filter(r => r.id !== repository.id)
    this.emitUpdate()
  }
}

AccountsStore

File: app/src/lib/stores/accounts-store.ts Manages user accounts and tokens:
export class AccountsStore {
  private accounts: ReadonlyArray<Account> = []
  
  public async loadAccounts(): Promise<ReadonlyArray<Account>> {
    // Load accounts from secure storage
    const accounts = await loadAccountsFromKeychain()
    this.accounts = accounts
    this.emitUpdate()
    return accounts
  }
  
  public async addAccount(account: Account): Promise<void> {
    // Store token securely
    await storeToken(account.endpoint, account.token)
    
    this.accounts = [...this.accounts, account]
    this.emitUpdate()
  }
  
  public getAccountForEndpoint(endpoint: string): Account | null {
    return this.accounts.find(a => a.endpoint === endpoint) || null
  }
}

PullRequestStore

File: app/src/lib/stores/pull-request-store.ts Caches pull request data:
export class PullRequestStore {
  private readonly cache = new Map<number, PullRequest>()
  
  public async fetchPullRequest(
    repository: RepositoryWithGitHubRepository,
    number: number
  ): Promise<PullRequest> {
    // Check cache first
    const cached = this.cache.get(number)
    if (cached && !this.isStale(cached)) {
      return cached
    }
    
    // Fetch from API
    const api = new API(repository.gitHubRepository.endpoint)
    const pr = await api.fetchPullRequest(repository.owner, repository.name, number)
    
    // Update cache
    this.cache.set(number, pr)
    return pr
  }
}

CommitStatusStore

File: app/src/lib/stores/commit-status-store.ts Tracks CI/CD check status:
export class CommitStatusStore {
  private readonly statusCache = new Map<string, ICombinedRefCheck>()
  
  public async fetchCommitStatus(
    repository: RepositoryWithGitHubRepository,
    sha: string
  ): Promise<ICombinedRefCheck> {
    // Fetch status from GitHub API
    const api = new API(repository.gitHubRepository.endpoint)
    const status = await api.fetchCombinedRefCheck(repository.owner, repository.name, sha)
    
    this.statusCache.set(sha, status)
    this.emitUpdate()
    return status
  }
  
  public subscribeToStatus(
    repository: RepositoryWithGitHubRepository,
    sha: string,
    callback: StatusCallBack
  ): Disposable {
    // Subscribe to status updates
    return this.emitter.on(`status:${sha}`, callback)
  }
}

State Persistence

Local Storage

UI preferences stored in localStorage:
export function saveWindowState(state: WindowState): void {
  localStorage.setItem('window-state', JSON.stringify(state))
}

export function loadWindowState(): WindowState | null {
  const json = localStorage.getItem('window-state')
  return json ? JSON.parse(json) : null
}

IndexedDB

Structured data stored in IndexedDB via Dexie:
import Dexie from 'dexie'

export class RepositoriesDatabase extends Dexie {
  public repositories: Dexie.Table<Repository, number>
  
  public constructor() {
    super('RepositoriesDatabase')
    
    this.version(1).stores({
      repositories: '++id,path,lastOpened',
    })
    
    this.repositories = this.table('repositories')
  }
}

Secure Storage

Sensitive data (tokens) stored in system keychain:
import * as keytar from 'keytar'

export async function storeToken(
  endpoint: string,
  token: string
): Promise<void> {
  await keytar.setPassword('GitHub Desktop', endpoint, token)
}

export async function loadToken(endpoint: string): Promise<string | null> {
  return await keytar.getPassword('GitHub Desktop', endpoint)
}

Event System

Store Events

Stores emit events when state changes:
import { Emitter, Disposable } from 'event-kit'

export class BaseStore {
  protected readonly emitter = new Emitter()
  
  public onDidUpdate(callback: (state: any) => void): Disposable {
    return this.emitter.on('did-update', callback)
  }
  
  protected emitUpdate() {
    this.emitter.emit('did-update', this.getState())
  }
}

Component Subscriptions

Components subscribe to store updates:
export class MyComponent extends React.Component {
  private subscription: Disposable | null = null
  
  public componentDidMount() {
    this.subscription = this.props.appStore.onDidUpdate(state => {
      this.setState(state)
    })
  }
  
  public componentWillUnmount() {
    this.subscription?.dispose()
  }
}

Derived State

Memoized Selectors

Derived state computed on-demand:
import memoizeOne from 'memoize-one'

export class AppStore extends BaseStore {
  // Memoized selector for filtered branches
  private getFilteredBranches = memoizeOne(
    (
      branches: ReadonlyArray<Branch>,
      filter: string
    ): ReadonlyArray<Branch> => {
      return branches.filter(b =>
        b.name.toLowerCase().includes(filter.toLowerCase())
      )
    }
  )
  
  public getVisibleBranches(filter: string): ReadonlyArray<Branch> {
    const state = this.getRepositoryState(this.selectedRepository)
    return this.getFilteredBranches(state.branchesState.allBranches, filter)
  }
}

State Debugging

Dev Tools

Logging state changes in development:
if (process.env.NODE_ENV === 'development') {
  appStore.onDidUpdate(state => {
    console.log('State updated:', state)
  })
}

State Snapshots

Export state for debugging:
export function exportStateSnapshot(): string {
  const state = appStore.getState()
  return JSON.stringify(state, null, 2)
}

Performance Optimization

Memoization

Cache expensive derived state computations

Batching

Batch multiple updates into single render

Lazy Loading

Load repository data on-demand

Background Tasks

Refresh data in background without blocking UI

Update Batching

export class AppStore extends BaseStore {
  private updateScheduled = false
  
  private scheduleUpdate() {
    if (this.updateScheduled) {
      return
    }
    
    this.updateScheduled = true
    
    // Batch updates using microtask
    Promise.resolve().then(() => {
      this.updateScheduled = false
      this.emitUpdate()
    })
  }
}

Testing Stores

Mock Stores

import { AppStore } from '../stores/app-store'

const mockAppStore = {
  getState: () => ({
    repositories: [],
    accounts: [],
    selectedState: null,
  }),
  onDidUpdate: jest.fn(),
} as unknown as AppStore

Store Tests

describe('AppStore', () => {
  it('updates state immutably', () => {
    const store = new AppStore()
    const oldState = store.getState()
    
    store.selectRepository(repository)
    const newState = store.getState()
    
    expect(oldState).not.toBe(newState)
    expect(oldState.repositories).toBe(newState.repositories) // unchanged
  })
})

UI Components

See how components consume state

Git Operations

Learn how Git data enters stores

Build docs developers (and LLMs) love