Skip to content

Commit

Permalink
Merge branch 'master' into bump-to-npm-5-finally
Browse files Browse the repository at this point in the history
  • Loading branch information
shiftkey committed Oct 16, 2017
2 parents 3cbbdab + be3c404 commit b096011
Show file tree
Hide file tree
Showing 24 changed files with 481 additions and 52 deletions.
5 changes: 4 additions & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"productName": "GitHub Desktop",
"bundleID": "com.github.GitHubClient",
"companyName": "GitHub, Inc.",
"version": "1.0.4-beta1",
"version": "1.0.5-beta0",
"main": "./main.js",
"repository": {
"type": "git",
Expand All @@ -19,6 +19,7 @@
"dependencies": {
"app-path": "^2.2.0",
"byline": "^5.0.0",
"chalk": "^2.0.1",
"classnames": "^2.2.5",
"codemirror": "^5.29.0",
"deep-equal": "^1.0.1",
Expand All @@ -31,6 +32,7 @@
"fs-extra": "^2.1.2",
"keytar": "^4.0.4",
"moment": "^2.17.1",
"mri": "^1.1.0",
"primer-support": "^4.0.0",
"react": "^15.6.2",
"react-addons-shallow-compare": "^15.6.2",
Expand All @@ -39,6 +41,7 @@
"react-virtualized": "^9.10.1",
"runas": "^3.1.1",
"source-map-support": "^0.4.15",
"strip-ansi": "^4.0.0",
"textarea-caret": "^3.0.2",
"ua-parser-js": "^0.7.12",
"untildify": "^3.0.2",
Expand Down
47 changes: 47 additions & 0 deletions app/src/cli/commands/clone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as QueryString from 'querystring'
import { URL } from 'url'

import { CommandError } from '../util'
import { openDesktop } from '../open-desktop'
import { ICommandModule, mriArgv } from '../load-commands'

interface ICloneArgs extends mriArgv {
readonly branch?: string
}

const command: ICommandModule = {
command: 'clone <url|slug>',
description: 'Clone a repository',
args: [
{
name: 'url|slug',
required: true,
description: 'The URL or the GitHub owner/name alias to clone',
type: 'string',
},
],
options: {
branch: {
type: 'string',
aliases: ['b'],
description: 'The branch to checkout after cloning',
},
},
handler({ _: [cloneUrl], branch }: ICloneArgs) {
if (!cloneUrl) {
throw new CommandError('Clone URL must be specified')
}
try {
const _ = new URL(cloneUrl)
_.toString() // don’t mark as unused
} catch (e) {
// invalid URL, assume a GitHub repo
cloneUrl = `https://github.com/${cloneUrl}`
}
const url = `openRepo/${cloneUrl}?${QueryString.stringify({
branch,
})}`
openDesktop(url)
},
}
export = command
80 changes: 80 additions & 0 deletions app/src/cli/commands/help.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import * as chalk from 'chalk'

import { commands, ICommandModule, IOption } from '../load-commands'

import { dasherizeOption, printTable } from '../util'

const command: ICommandModule = {
command: 'help [command]',
description: 'Show the help page for a command',
handler({ _: [command] }) {
if (command) {
printCommandHelp(command, commands[command])
} else {
printHelp()
}
},
}
export = command

function printHelp() {
console.log(chalk.underline('Commands:'))
const table: string[][] = []
for (const commandName of Object.keys(commands)) {
const command = commands[commandName]
table.push([chalk.bold(command.command), command.description])
}
printTable(table)
console.log(
`\nRun ${chalk.bold(
`github help ${chalk.gray('<command>')}`
)} for details about each command`
)
}

function printCommandHelp(name: string, command: ICommandModule) {
if (!command) {
console.log(`Unrecognized command: ${chalk.bold.red.underline(name)}`)
printHelp()
return
}
console.log(`${chalk.gray('github')} ${command.command}`)
if (command.aliases) {
for (const alias of command.aliases) {
console.log(chalk.gray(`github ${alias}`))
}
}
console.log()
const [title, body] = command.description.split('\n', 1)
console.log(chalk.bold(title))
if (body) {
console.log(body)
}
const { options, args } = command
if (options) {
console.log(chalk.underline('\nOptions:'))
printTable(
Object.keys(options)
.map(k => [k, options[k]] as [string, IOption])
.map(([optionName, option]) => [
[optionName, ...(option.aliases || [])]
.map(dasherizeOption)
.map(x => chalk.bold.blue(x))
.join(chalk.gray(', ')),
option.description,
chalk.gray(`[${chalk.underline(option.type)}]`),
])
)
}
if (args && args.length) {
console.log(chalk.underline('\nArguments:'))
printTable(
args.map(arg => [
(arg.required ? chalk.bold : chalk).blue(arg.name),
arg.required ? chalk.gray('(required)') : '',
arg.description,
chalk.gray(`[${chalk.underline(arg.type)}]`),
])
)
}
}
33 changes: 33 additions & 0 deletions app/src/cli/commands/open.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as Path from 'path'

import { ICommandModule, mriArgv } from '../load-commands'
import { openDesktop } from '../open-desktop'

interface IOpenArgs extends mriArgv {
readonly path: string
}

const command: ICommandModule = {
command: 'open <path>',
aliases: ['<path>'],
description: 'Open a git repository in GitHub Desktop',
args: [
{
name: 'path',
description: 'The path to the repository to open',
type: 'string',
required: false,
},
],
handler({ _: [pathArg] }: IOpenArgs) {
if (!pathArg) {
// just open Desktop
openDesktop()
return
}
const repositoryPath = Path.resolve(process.cwd(), pathArg)
const url = `openLocalRepo/${encodeURIComponent(repositoryPath)}`
openDesktop(url)
},
}
export = command
6 changes: 6 additions & 0 deletions app/src/cli/dev-commands-global.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const Fs = require('fs')
const path = require('path')

const distInfo = require('../../../script/dist-info')

global.__CLI_COMMANDS__ = distInfo.getCLICommands()
50 changes: 50 additions & 0 deletions app/src/cli/load-commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Argv as mriArgv } from 'mri'

import { TypeName } from './util'

type StringArray = ReadonlyArray<string>

export type CommandHandler = (args: mriArgv, argv: StringArray) => void
export { mriArgv }

export interface IOption {
readonly type: TypeName
readonly aliases?: StringArray
readonly description: string
readonly default?: any
}

interface IArgument {
readonly name: string
readonly required: boolean
readonly description: string
readonly type: TypeName
}

export interface ICommandModule {
name?: string
readonly command: string
readonly description: string
readonly handler: CommandHandler
readonly aliases?: StringArray
readonly options?: { [flag: string]: IOption }
readonly args?: ReadonlyArray<IArgument>
readonly unknownOptionHandler?: (flag: string) => void
}

function loadModule(name: string): ICommandModule {
return require(`./commands/${name}.ts`)
}

interface ICommands {
[command: string]: ICommandModule
}
export const commands: ICommands = {}

for (const fileName of __CLI_COMMANDS__) {
const mod = loadModule(fileName)
if (!mod.name) {
mod.name = fileName
}
commands[mod.name] = mod
}
126 changes: 104 additions & 22 deletions app/src/cli/main.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,105 @@
import * as ChildProcess from 'child_process'
import * as Path from 'path'

const args = process.argv.slice(2)

// At some point we may have other command line options, but for now we assume
// the first arg is the path to open.
const pathArg = args.length > 0 ? args[0] : null
const repositoryPath = pathArg ? Path.resolve(process.cwd(), pathArg) : ''
const url = `x-github-client://openLocalRepo/${encodeURIComponent(
repositoryPath
)}`

const env = { ...process.env }
// NB: We're gonna launch Desktop and we definitely don't want to carry over
// `ELECTRON_RUN_AS_NODE`. This seems to only happen on Windows.
delete env['ELECTRON_RUN_AS_NODE']

if (__DARWIN__) {
ChildProcess.spawn('open', [url], { env })
} else if (__WIN32__) {
ChildProcess.spawn('cmd', ['/c', 'start', url], { env })
import * as mri from 'mri'
import * as chalk from 'chalk'

import { dasherizeOption, CommandError } from './util'
import { commands } from './load-commands'
const defaultCommand = 'open'

let args = process.argv.slice(2)
if (!args[0]) {
args[0] = '.'
}
const commandArg = args[0]
args = args.slice(1)

// tslint:disable-next-line whitespace
;(function attemptRun(name: string) {
try {
if (supportsCommand(name)) {
runCommand(name)
} else if (name.startsWith('--')) {
attemptRun(name.slice(2))
} else {
try {
args.unshift(commandArg)
runCommand(defaultCommand)
} catch (err) {
logError(err)
args = []
runCommand('help')
}
}
} catch (err) {
logError(err)
args = [name]
runCommand('help')
}
})(commandArg)

function logError(err: CommandError) {
console.log(chalk.bgBlack.red('ERR!'), err.message)
if (err.stack && !err.pretty) {
console.log(chalk.gray(err.stack))
}
}

console.log() // nice blank line before the command prompt

interface IMRIOpts extends mri.Options {
alias: mri.DictionaryObject<mri.ArrayOrString>
boolean: Array<string>
default: mri.DictionaryObject
string: Array<string>
}

function runCommand(name: string) {
const command = commands[name]
const opts: IMRIOpts = {
alias: {},
boolean: [],
default: {},
string: [],
}
if (command.options) {
for (const flag of Object.keys(command.options)) {
const flagOptions = command.options[flag]
if (flagOptions.aliases) {
opts.alias[flag] = flagOptions.aliases
}
if (flagOptions.hasOwnProperty('default')) {
opts.default[flag] = flagOptions.default
}
switch (flagOptions.type) {
case 'string':
opts.string.push(flag)
break
case 'boolean':
opts.boolean.push(flag)
break
}
}
opts.unknown = command.unknownOptionHandler
}
const parsedArgs = mri(args, opts)
if (command.options) {
for (const flag of Object.keys(parsedArgs)) {
if (!(flag in command.options)) {
continue
}

const value = parsedArgs[flag]
const expectedType = command.options[flag].type
if (typeof value !== expectedType) {
throw new CommandError(
`Value passed to flag ${dasherizeOption(
flag
)} was of type ${typeof value}, but was expected to be of type ${expectedType}`
)
}
}
}
command.handler(parsedArgs, args)
}
function supportsCommand(name: string) {
return Object.prototype.hasOwnProperty.call(commands, name)
}
Loading

0 comments on commit b096011

Please sign in to comment.