Code Study: How URL Action Works in Github Desktop

Understanding URL Action

First you need to understand what URL action is, and how a App URL string is parsed into a URL action.

parse-app-url.ts defines the supported URL action types.

export type URLActionType =
  | IOAuthAction
  | IOpenRepositoryFromURLAction
  | IOpenRepositoryFromPathAction
  | IUnknownAction

export interface IOAuthAction {
  readonly name: 'oauth'
  readonly code: string
  readonly state: string
}

export interface IOpenRepositoryFromURLAction {
  readonly name: 'open-repository-from-url'
  /** the remote repository location associated with the "Open in Desktop" action */
  readonly url: string
  /** the optional branch name which should be checked out. use the default branch otherwise. */
  readonly branch: string | null
  /** the pull request number, if pull request originates from a fork of the repository */
  readonly pr: string | null
  /** the file to open after cloning the repository */
  readonly filepath: string | null
}

export interface IOpenRepositoryFromPathAction {
  readonly name: 'open-repository-from-path'
  /** The local path to open. */
  readonly path: string
}

export interface IUnknownAction {
  readonly name: 'unknown'
  readonly url: string
}

It also defines a parseAppURL function to parse a given app url string to one of the URL action types with extracted parameters for each URL action type.

export function parseAppURL(url: string): URLActionType

The app url string is in the format of: <protocol>://<actionName>/<parsedPath>?<queryStrings>. Here are the format and example url strings of each URLActionType:

Triggering URL Action

A URL action is triggered from two possible places in the app.

  1. Through the app protocol and the protocol launcher arguments

    Let’s look at how GitHub Desktop registers itself as the default handler of the ‘x-github-client’ protocol, so that when someone clicks x-github-client://???, GitHub Desktop will be launched with the parameter. After GitHub Desktop is launched for the first time, it will set itself as the default protocol for x-github-client. This is done in the app.on(‘ready’ callback in main.ts

     app.on('ready', () => {
       setAsDefaultProtocolClient('x-github-client')
    

    After this registration, if someone clicks x-github-client://openRepo/???, Windows will launch GitHub Desktop with this command: GitHubDesktop.exe --protocol-launcher x-github-client://openRepo/???. macOS and Linux will launch GitHub Desktop with this command: GitHubDesktop.exe x-github-client://openRepo/???.

    When GitHub Desktop launches, main.ts calls handlePossibleProtocolLauncherArgs to parse and handle the protocol launcher args.

     function handlePossibleProtocolLauncherArgs(args: ReadonlyArray<string>) {
       log.info(`Received possible protocol arguments: ${args.length}`)
    
       if (__WIN32__) {
         // Desktop registers it's protocol handler callback on Windows as
         // `[executable path] --protocol-launcher "%1"`. At launch it checks
         // for that exact scenario here before doing any processing, and only
         // processing the first argument. If there's more than 3 args because of a
         // malformed or untrusted url then we bail out.
         if (args.length === 3 && args[1] === '--protocol-launcher') {
           handleAppURL(args[2])
         }
       } else if (args.length > 1) {
         handleAppURL(args[1])
       }
     }
    
  2. In macOS only,

app.on('will-finish-launching', () => {
  // macOS only
  app.on('open-url', (event, url) => {
    event.preventDefault()
    handleAppURL(url)
  })
})

if (__DARWIN__) {
  app.on('open-file', async (event, path) => {
    event.preventDefault()
    Fs.stat(path, (err, stats) => {
      ...
      handleAppURL(`jify://openLocalRepo/${encodeURIComponent(path)}`)
    })
  })
}

Handling URL Action

function handleAppURL(url: string) {
  log.info('Processing protocol url')
  const action = parseAppURL(url)
  onDidLoad(window => {
    // This manual focus call _shouldn't_ be necessary, but is for Chrome on
    // macOS. See https://github.com/desktop/desktop/issues/973.
    window.focus()
    window.sendURLAction(action)
  })
}

The AppWindow class defined in app-window.ts offers the sendURLAction method to pass the ‘url-action’ event message with the action detail from the main process to renderer.

public sendURLAction(action: URLActionType) {
  this.show()
  this.window.webContents.send('url-action', { action })
}

index.tsx in the renderer process listens to the ‘url-action’ event.

ipcRenderer.on(
  'url-action',
  (event: Electron.IpcMessageEvent, { action }: { action: URLActionType }) => {
    dispatcher.dispatchURLAction(action)
  }
)

It receives the action detail and dispatches the action. The action is taken in the renderer process.

public async dispatchURLAction(action: URLActionType): Promise<void> {
  switch (action.name) {
    case 'oauth':
      ...
      break
    case 'open-repository-from-url':
      const { url } = action
      const repository = await this.openRepository(url)
      if (repository) {
        await this.handleCloneInDesktopOptions(repository, action)
      } else {
        log.warn(
          `Open Repository from URL failed, did not find repository: ${url} - payload: ${JSON.stringify(
            action
          )}`
        )
      }
      break
    case 'open-repository-from-path':
      ...
      break
    default:
      ...
  }
}
tags: GitHub Desktop - Electron