mirror of
https://github.com/desktop/desktop
synced 2024-09-13 21:31:32 +00:00
Fix conflicts and address feedback
This commit is contained in:
parent
a3bc4967cb
commit
cde2ae8bf0
2
CONTRIBUTING.md → .github/CONTRIBUTING.md
vendored
2
CONTRIBUTING.md → .github/CONTRIBUTING.md
vendored
|
@ -151,7 +151,7 @@ pull requests.
|
|||
| `enhancement` | [search](https://github.com/desktop/desktop/labels/enhancement) | Feature requests. |
|
||||
| `bug` | [search](https://github.com/desktop/desktop/labels/bug) | Confirmed bugs or reports that are very likely to be bugs. |
|
||||
| `more-information-needed` | [search](https://github.com/desktop/desktop/labels/more-information-needed) | More information needs to be collected about these problems or feature requests (e.g. steps to reproduce). |
|
||||
| `needs-reproduction` | [search](https://github.com/desktop/desktop/labels/needs-reproduction) | Likely bugs, but haven't been reliably reproduced. |
|
||||
| `reviewer-needs-to-reproduce` | [search](https://github.com/desktop/desktop/labels/reviewer-needs-to-reproduce) | Likely bugs, but haven't been reliably reproduced by a reviewer. |
|
||||
| `stale` | [search](https://github.com/desktop/desktop/labels/stale) | Issues that are inactive and marked to be closed. |
|
||||
| `macOS` | [search](https://github.com/desktop/desktop/labels/macOS) | Issues specific to macOS users. |
|
||||
| `Windows` | [search](https://github.com/desktop/desktop/labels/Windows) | Issues specific to Windows users. |
|
11
.github/config.yml
vendored
11
.github/config.yml
vendored
|
@ -2,9 +2,14 @@
|
|||
|
||||
# *Required* Comment to reply with
|
||||
requestInfoReplyComment: >
|
||||
We require the template to be filled out on all new issues. We do this so that we can be certain we have
|
||||
all the information we need to address your submission efficiently. This allows the maintainers to spend
|
||||
more time fixing bugs, implementing enhancements, and reviewing and merging pull requests.
|
||||
Thanks for reaching out!
|
||||
|
||||
We require the [template](https://github.com/desktop/desktop/blob/master/.github/CONTRIBUTING.md#how-do-i-submit-a-good-bug-report) to be filled out with all new issues. We do this so that
|
||||
we can be certain we have all the information we need to address your
|
||||
submission efficiently. This allows the maintainers to spend more time fixing
|
||||
bugs, implementing enhancements, and reviewing and merging pull requests.
|
||||
|
||||
Thanks for understanding and meeting us halfway 😀
|
||||
|
||||
requestInfoLabelToAdd: more-information-needed
|
||||
|
||||
|
|
|
@ -48,13 +48,16 @@ First, please search the [open issues](https://github.com/desktop/desktop/issues
|
|||
and [closed issues](https://github.com/desktop/desktop/issues?q=is%3Aclosed)
|
||||
to see if your issue hasn't already been reported (it may also be fixed).
|
||||
|
||||
There is also a list of [known issues](https://github.com/desktop/desktop/blob/master/docs/known-issues.md)
|
||||
that are being tracked against Desktop, and some of these issues have workarounds.
|
||||
|
||||
If you can't find an issue that matches what you're seeing, open a [new issue](https://github.com/desktop/desktop/issues/new)
|
||||
and fill out the template to provide us with enough information to investigate
|
||||
further.
|
||||
|
||||
## How can I contribute to GitHub Desktop?
|
||||
|
||||
The [CONTRIBUTING.md](./CONTRIBUTING.md) document will help you get setup and
|
||||
The [CONTRIBUTING.md](./.github/CONTRIBUTING.md) document will help you get setup and
|
||||
familiar with the source. The [documentation](docs/) folder also contains more
|
||||
resources relevant to the project.
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"productName": "GitHub Desktop",
|
||||
"bundleID": "com.github.GitHubClient",
|
||||
"companyName": "GitHub, Inc.",
|
||||
"version": "1.1.0-beta3",
|
||||
"version": "1.1.1-beta1",
|
||||
"main": "./main.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { spawn } from 'child_process'
|
||||
import * as Path from 'path'
|
||||
|
||||
export function isGitOnPath(): Promise<boolean> {
|
||||
// Modern versions of macOS ship with a Git shim that guides you through
|
||||
|
@ -12,16 +13,19 @@ export function isGitOnPath(): Promise<boolean> {
|
|||
// adapted from http://stackoverflow.com/a/34953561/1363815
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
if (__WIN32__) {
|
||||
const process = spawn('C:\\Windows\\System32\\where.exe', ['git'])
|
||||
const windowsRoot = process.env.SystemRoot || 'C:\\Windows'
|
||||
const wherePath = Path.join(windowsRoot, 'System32', 'where.exe')
|
||||
|
||||
process.on('error', error => {
|
||||
const cp = spawn(wherePath, ['git'])
|
||||
|
||||
cp.on('error', error => {
|
||||
log.warn('Unable to spawn where.exe', error)
|
||||
resolve(false)
|
||||
})
|
||||
|
||||
// `where` will return 0 when the executable
|
||||
// is found under PATH, or 1 if it cannot be found
|
||||
process.on('close', function(code) {
|
||||
cp.on('close', function(code) {
|
||||
resolve(code === 0)
|
||||
})
|
||||
return
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { spawn, ChildProcess } from 'child_process'
|
||||
import * as Path from 'path'
|
||||
import { assertNever } from '../fatal-error'
|
||||
import { enumerateValues, HKEY, RegistryValueType } from 'registry-js'
|
||||
|
||||
import { pathExists } from '../file-system'
|
||||
import { assertNever } from '../fatal-error'
|
||||
import { IFoundShell } from './found-shell'
|
||||
|
||||
export enum Shell {
|
||||
|
@ -43,75 +45,127 @@ export async function getAvailableShells(): Promise<
|
|||
},
|
||||
]
|
||||
|
||||
const powerShellPath = await findPowerShell()
|
||||
if (powerShellPath != null) {
|
||||
shells.push({
|
||||
shell: Shell.PowerShell,
|
||||
path: powerShellPath,
|
||||
})
|
||||
}
|
||||
|
||||
const hyperPath = await findHyper()
|
||||
if (hyperPath != null) {
|
||||
shells.push({
|
||||
shell: Shell.Hyper,
|
||||
path: hyperPath,
|
||||
})
|
||||
}
|
||||
|
||||
const gitBashPath = await findGitBash()
|
||||
if (gitBashPath != null) {
|
||||
shells.push({
|
||||
shell: Shell.GitBash,
|
||||
path: gitBashPath,
|
||||
})
|
||||
}
|
||||
|
||||
return shells
|
||||
}
|
||||
|
||||
async function findPowerShell(): Promise<string | null> {
|
||||
const powerShell = enumerateValues(
|
||||
HKEY.HKEY_LOCAL_MACHINE,
|
||||
'Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\PowerShell.exe'
|
||||
)
|
||||
if (powerShell.length > 0) {
|
||||
const first = powerShell[0]
|
||||
|
||||
// NOTE:
|
||||
// on Windows 7 these are both REG_SZ, which technically isn't supposed
|
||||
// to contain unexpanded references to environment variables. But given
|
||||
// it's also %SystemRoot% and we do the expanding here I think this is
|
||||
// a fine workaround to do to support the maximum number of setups.
|
||||
if (powerShell.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (
|
||||
first.type === RegistryValueType.REG_EXPAND_SZ ||
|
||||
first.type === RegistryValueType.REG_SZ
|
||||
) {
|
||||
const path = first.data.replace(
|
||||
/^%SystemRoot%/i,
|
||||
process.env.SystemRoot || 'C:\\Windows'
|
||||
const first = powerShell[0]
|
||||
|
||||
// NOTE:
|
||||
// on Windows 7 these are both REG_SZ, which technically isn't supposed
|
||||
// to contain unexpanded references to environment variables. But given
|
||||
// it's also %SystemRoot% and we do the expanding here I think this is
|
||||
// a fine workaround to do to support the maximum number of setups.
|
||||
|
||||
if (
|
||||
first.type === RegistryValueType.REG_EXPAND_SZ ||
|
||||
first.type === RegistryValueType.REG_SZ
|
||||
) {
|
||||
const path = first.data.replace(
|
||||
/^%SystemRoot%/i,
|
||||
process.env.SystemRoot || 'C:\\Windows'
|
||||
)
|
||||
|
||||
if (await pathExists(path)) {
|
||||
return path
|
||||
} else {
|
||||
log.debug(
|
||||
`[PowerShell] registry entry found but does not exist at '${path}'`
|
||||
)
|
||||
shells.push({
|
||||
shell: Shell.PowerShell,
|
||||
path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async function findHyper(): Promise<string | null> {
|
||||
const hyper = enumerateValues(
|
||||
HKEY.HKEY_CURRENT_USER,
|
||||
'Software\\Classes\\Directory\\Background\\shell\\Hyper\\command'
|
||||
)
|
||||
if (hyper.length > 0) {
|
||||
const first = hyper[0]
|
||||
if (first.type === RegistryValueType.REG_SZ) {
|
||||
// Registry key is structured as "{installationPath}\app-x.x.x\Hyper.exe" "%V"
|
||||
|
||||
// This regex is designed to get the path to the version-specific Hyper.
|
||||
// commandPieces = ['"{installationPath}\app-x.x.x\Hyper.exe"', '"', '{installationPath}\app-x.x.x\Hyper.exe', ...]
|
||||
const commandPieces = first.data.match(/(["'])(.*?)\1/)
|
||||
const path = commandPieces
|
||||
? commandPieces[2]
|
||||
: process.env.LocalAppData.concat('\\hyper\\Hyper.exe') // fall back to the launcher in install root
|
||||
shells.push({
|
||||
shell: Shell.Hyper,
|
||||
path: path,
|
||||
})
|
||||
if (hyper.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const first = hyper[0]
|
||||
if (first.type === RegistryValueType.REG_SZ) {
|
||||
// Registry key is structured as "{installationPath}\app-x.x.x\Hyper.exe" "%V"
|
||||
|
||||
// This regex is designed to get the path to the version-specific Hyper.
|
||||
// commandPieces = ['"{installationPath}\app-x.x.x\Hyper.exe"', '"', '{installationPath}\app-x.x.x\Hyper.exe', ...]
|
||||
const commandPieces = first.data.match(/(["'])(.*?)\1/)
|
||||
const path = commandPieces
|
||||
? commandPieces[2]
|
||||
: process.env.LocalAppData.concat('\\hyper\\Hyper.exe') // fall back to the launcher in install root
|
||||
|
||||
if (await pathExists(path)) {
|
||||
return path
|
||||
} else {
|
||||
log.debug(`[Hyper] registry entry found but does not exist at '${path}'`)
|
||||
}
|
||||
}
|
||||
|
||||
const gitBash = enumerateValues(
|
||||
return null
|
||||
}
|
||||
|
||||
async function findGitBash(): Promise<string | null> {
|
||||
const registryPath = enumerateValues(
|
||||
HKEY.HKEY_LOCAL_MACHINE,
|
||||
'SOFTWARE\\GitForWindows'
|
||||
)
|
||||
|
||||
if (gitBash.length > 0) {
|
||||
const installPathEntry = gitBash.find(e => e.name === 'InstallPath')
|
||||
if (
|
||||
installPathEntry &&
|
||||
installPathEntry.type === RegistryValueType.REG_SZ
|
||||
) {
|
||||
shells.push({
|
||||
shell: Shell.GitBash,
|
||||
path: Path.join(installPathEntry.data, 'git-bash.exe'),
|
||||
})
|
||||
if (registryPath.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const installPathEntry = registryPath.find(e => e.name === 'InstallPath')
|
||||
if (installPathEntry && installPathEntry.type === RegistryValueType.REG_SZ) {
|
||||
const path = Path.join(installPathEntry.data, 'git-bash.exe')
|
||||
|
||||
if (await pathExists(path)) {
|
||||
return path
|
||||
} else {
|
||||
log.debug(
|
||||
`[Git Bash] registry entry found but does not exist at '${path}'`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return shells
|
||||
return null
|
||||
}
|
||||
|
||||
export function launch(
|
||||
|
|
|
@ -535,9 +535,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
}
|
||||
|
||||
public componentDidMount() {
|
||||
document.ondragover = document.ondrop = e => {
|
||||
e.preventDefault()
|
||||
}
|
||||
document.ondragover = document.ondrop = e => e.preventDefault()
|
||||
|
||||
document.body.ondrop = e => {
|
||||
if (this.state.currentPopup != null) {
|
||||
|
@ -870,9 +868,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
)
|
||||
}
|
||||
|
||||
private onPopupDismissed = () => {
|
||||
this.props.dispatcher.closePopup()
|
||||
}
|
||||
private onPopupDismissed = () => this.props.dispatcher.closePopup()
|
||||
|
||||
private onSignInDialogDismissed = () => {
|
||||
this.props.dispatcher.resetSignInState()
|
||||
|
@ -889,9 +885,8 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
)
|
||||
}
|
||||
|
||||
private onUpdateAvailableDismissed = () => {
|
||||
private onUpdateAvailableDismissed = () =>
|
||||
this.props.dispatcher.setUpdateBannerVisibility(false)
|
||||
}
|
||||
|
||||
private currentPopupContent(): JSX.Element | null {
|
||||
// Hide any dialogs while we're displaying an error
|
||||
|
@ -1253,9 +1248,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
this.props.dispatcher.performRetry(retryAction)
|
||||
}
|
||||
|
||||
private onCheckForUpdates = () => {
|
||||
this.checkForUpdates(false)
|
||||
}
|
||||
private onCheckForUpdates = () => this.checkForUpdates(false)
|
||||
|
||||
private showAcknowledgements = () => {
|
||||
this.props.dispatcher.showPopup({ type: PopupType.Acknowledgements })
|
||||
|
@ -1286,9 +1279,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
return <FullScreenInfo windowState={this.state.windowState} />
|
||||
}
|
||||
|
||||
private clearError = (error: Error) => {
|
||||
this.props.dispatcher.clearError(error)
|
||||
}
|
||||
private clearError = (error: Error) => this.props.dispatcher.clearError(error)
|
||||
|
||||
private onConfirmDiscardChangesChanged = (value: boolean) => {
|
||||
this.props.dispatcher.setConfirmDiscardChangesSetting(value)
|
||||
|
|
|
@ -24,6 +24,9 @@ interface IAutocompletingTextInputProps<ElementType> {
|
|||
/** The current value of the input field. */
|
||||
readonly value?: string
|
||||
|
||||
/** Disabled state for input field. */
|
||||
readonly disabled?: boolean
|
||||
|
||||
/**
|
||||
* Called when the user changes the value in the input field.
|
||||
*/
|
||||
|
@ -197,7 +200,7 @@ export abstract class AutocompletingTextInput<
|
|||
const item = currentAutoCompletionState.items[row]
|
||||
|
||||
if (item) {
|
||||
this.insertCompletion(item)
|
||||
this.insertCompletion(item, 'mouseclick')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -231,16 +234,7 @@ export abstract class AutocompletingTextInput<
|
|||
|
||||
const item = items[row]
|
||||
|
||||
this.insertCompletion(item)
|
||||
|
||||
// This is pretty gross. Clicking on the list moves focus off the text area.
|
||||
// Immediately moving focus back doesn't work. Gotta wait a runloop I guess?
|
||||
window.setTimeout(() => {
|
||||
const element = this.element
|
||||
if (element) {
|
||||
element.focus()
|
||||
}
|
||||
}, 0)
|
||||
this.insertCompletion(item, 'mouseclick')
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -258,6 +252,7 @@ export abstract class AutocompletingTextInput<
|
|||
onChange: this.onChange,
|
||||
onKeyDown: this.onKeyDown,
|
||||
onBlur: this.onBlur,
|
||||
disabled: this.props.disabled,
|
||||
}
|
||||
|
||||
return React.createElement<React.HTMLAttributes<ElementType>, ElementType>(
|
||||
|
@ -302,7 +297,17 @@ export abstract class AutocompletingTextInput<
|
|||
)
|
||||
}
|
||||
|
||||
private insertCompletion(item: Object) {
|
||||
private setCursorPosition(newCaretPosition: number) {
|
||||
if (this.element == null) {
|
||||
log.warn('Unable to set cursor position when element is null')
|
||||
return
|
||||
}
|
||||
|
||||
this.element.selectionStart = newCaretPosition
|
||||
this.element.selectionEnd = newCaretPosition
|
||||
}
|
||||
|
||||
private insertCompletion(item: Object, source: 'mouseclick' | 'keyboard') {
|
||||
const element = this.element!
|
||||
const autocompletionState = this.state.autocompletionState!
|
||||
const originalText = element.value
|
||||
|
@ -310,17 +315,32 @@ export abstract class AutocompletingTextInput<
|
|||
const autoCompleteText = autocompletionState.provider.getCompletionText(
|
||||
item
|
||||
)
|
||||
|
||||
const textWithAutoCompleteText =
|
||||
originalText.substr(0, range.start - 1) + autoCompleteText + ' '
|
||||
|
||||
const newText =
|
||||
originalText.substr(0, range.start - 1) +
|
||||
autoCompleteText +
|
||||
originalText.substr(range.start + range.length) +
|
||||
' '
|
||||
textWithAutoCompleteText + originalText.substr(range.start + range.length)
|
||||
|
||||
element.value = newText
|
||||
|
||||
if (this.props.onValueChanged) {
|
||||
this.props.onValueChanged(newText)
|
||||
}
|
||||
|
||||
const newCaretPosition = textWithAutoCompleteText.length
|
||||
|
||||
if (source === 'mouseclick') {
|
||||
// we only need to re-focus on the text input when the autocomplete overlay
|
||||
// steals focus due to the user clicking on a selection in the autocomplete list
|
||||
window.setTimeout(() => {
|
||||
element.focus()
|
||||
this.setCursorPosition(newCaretPosition)
|
||||
}, 0)
|
||||
} else {
|
||||
this.setCursorPosition(newCaretPosition)
|
||||
}
|
||||
|
||||
this.close()
|
||||
}
|
||||
|
||||
|
@ -385,7 +405,7 @@ export abstract class AutocompletingTextInput<
|
|||
if (item) {
|
||||
event.preventDefault()
|
||||
|
||||
this.insertCompletion(item)
|
||||
this.insertCompletion(item, 'keyboard')
|
||||
}
|
||||
} else if (event.key === 'Escape') {
|
||||
this.close()
|
||||
|
|
|
@ -328,6 +328,7 @@ export class CommitMessage extends React.Component<
|
|||
onAuthorsUpdated={this.onCoAuthorsUpdated}
|
||||
authors={this.props.coAuthors}
|
||||
autoCompleteProvider={autocompletionProvider}
|
||||
disabled={this.props.isCommitting}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -352,7 +353,9 @@ export class CommitMessage extends React.Component<
|
|||
{
|
||||
label: this.toggleCoAuthorsText,
|
||||
action: this.onToggleCoAuthors,
|
||||
enabled: this.props.repository.gitHubRepository !== null,
|
||||
enabled:
|
||||
this.props.repository.gitHubRepository !== null &&
|
||||
!this.props.isCommitting,
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -360,7 +363,7 @@ export class CommitMessage extends React.Component<
|
|||
}
|
||||
|
||||
private onCoAuthorToggleButtonClick = (
|
||||
e: React.MouseEvent<HTMLDivElement>
|
||||
e: React.MouseEvent<HTMLButtonElement>
|
||||
) => {
|
||||
e.preventDefault()
|
||||
this.onToggleCoAuthors()
|
||||
|
@ -372,15 +375,15 @@ export class CommitMessage extends React.Component<
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
<button
|
||||
className="co-authors-toggle"
|
||||
onClick={this.onCoAuthorToggleButtonClick}
|
||||
tabIndex={-1}
|
||||
aria-label={this.toggleCoAuthorsText}
|
||||
disabled={this.props.isCommitting}
|
||||
>
|
||||
<Octicon symbol={addAuthorIcon} />
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -436,7 +439,11 @@ export class CommitMessage extends React.Component<
|
|||
return null
|
||||
}
|
||||
|
||||
return <div className="action-bar">{this.renderCoAuthorToggleButton()}</div>
|
||||
const className = classNames('action-bar', {
|
||||
disabled: this.props.isCommitting,
|
||||
})
|
||||
|
||||
return <div className={className}>{this.renderCoAuthorToggleButton()}</div>
|
||||
}
|
||||
|
||||
public render() {
|
||||
|
@ -471,6 +478,7 @@ export class CommitMessage extends React.Component<
|
|||
value={this.state.summary}
|
||||
onValueChanged={this.onSummaryChanged}
|
||||
autocompletionProviders={this.props.autocompletionProviders}
|
||||
disabled={this.props.isCommitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -486,6 +494,7 @@ export class CommitMessage extends React.Component<
|
|||
autocompletionProviders={this.props.autocompletionProviders}
|
||||
ref={this.onDescriptionFieldRef}
|
||||
onElementRef={this.onDescriptionTextAreaRef}
|
||||
disabled={this.props.isCommitting}
|
||||
/>
|
||||
{this.renderActionBar()}
|
||||
</FocusContainer>
|
||||
|
|
|
@ -253,7 +253,7 @@ export class CreateBranch extends React.Component<
|
|||
label="Name"
|
||||
value={this.state.proposedName}
|
||||
autoFocus={true}
|
||||
onChange={this.onBranchNameChange}
|
||||
onValueChanged={this.onBranchNameChange}
|
||||
/>
|
||||
</Row>
|
||||
|
||||
|
@ -277,9 +277,8 @@ export class CreateBranch extends React.Component<
|
|||
)
|
||||
}
|
||||
|
||||
private onBranchNameChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
const str = event.currentTarget.value
|
||||
this.updateBranchName(str)
|
||||
private onBranchNameChange = (name: string) => {
|
||||
this.updateBranchName(name)
|
||||
}
|
||||
|
||||
private updateBranchName(name: string) {
|
||||
|
|
|
@ -83,14 +83,14 @@ export class AuthenticationForm extends React.Component<
|
|||
label="Username or email address"
|
||||
disabled={disabled}
|
||||
autoFocus={true}
|
||||
onChange={this.onUsernameChange}
|
||||
onValueChanged={this.onUsernameChange}
|
||||
/>
|
||||
|
||||
<TextBox
|
||||
label="Password"
|
||||
type="password"
|
||||
disabled={disabled}
|
||||
onChange={this.onPasswordChange}
|
||||
onValueChanged={this.onPasswordChange}
|
||||
/>
|
||||
|
||||
{this.renderError()}
|
||||
|
@ -173,12 +173,12 @@ export class AuthenticationForm extends React.Component<
|
|||
return <Errors>{error.message}</Errors>
|
||||
}
|
||||
|
||||
private onUsernameChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
this.setState({ username: event.currentTarget.value })
|
||||
private onUsernameChange = (username: string) => {
|
||||
this.setState({ username })
|
||||
}
|
||||
|
||||
private onPasswordChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
this.setState({ password: event.currentTarget.value })
|
||||
private onPasswordChange = (password: string) => {
|
||||
this.setState({ password })
|
||||
}
|
||||
|
||||
private signInWithBrowser = (event?: React.MouseEvent<HTMLButtonElement>) => {
|
||||
|
|
|
@ -36,6 +36,12 @@ interface IAuthorInputProps {
|
|||
* input field.
|
||||
*/
|
||||
readonly onAuthorsUpdated: (authors: ReadonlyArray<IAuthor>) => void
|
||||
|
||||
/**
|
||||
* Whether or not the input should be read-only and styled as being
|
||||
* disabled. When disabled the component will not accept focus.
|
||||
*/
|
||||
readonly disabled: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -488,6 +494,12 @@ export class AuthorInput extends React.Component<IAuthorInputProps, {}> {
|
|||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: IAuthorInputProps) {
|
||||
const cm = this.editor
|
||||
|
||||
if (!cm) {
|
||||
return
|
||||
}
|
||||
|
||||
// If the authors prop have changed from our internal representation
|
||||
// we'll throw up our hands and reset the input to whatever we're
|
||||
// given.
|
||||
|
@ -495,12 +507,13 @@ export class AuthorInput extends React.Component<IAuthorInputProps, {}> {
|
|||
nextProps.authors !== this.props.authors &&
|
||||
!arrayEquals(this.authors, nextProps.authors)
|
||||
) {
|
||||
const cm = this.editor
|
||||
if (cm) {
|
||||
cm.operation(() => {
|
||||
this.reset(cm, nextProps.authors)
|
||||
})
|
||||
}
|
||||
cm.operation(() => {
|
||||
this.reset(cm, nextProps.authors)
|
||||
})
|
||||
}
|
||||
|
||||
if (nextProps.disabled !== this.props.disabled) {
|
||||
cm.setOption('readOnly', nextProps.disabled ? 'nocursor' : false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -702,6 +715,7 @@ export class AuthorInput extends React.Component<IAuthorInputProps, {}> {
|
|||
'Ctrl-Enter': false,
|
||||
'Cmd-Enter': false,
|
||||
},
|
||||
readOnly: this.props.disabled ? 'nocursor' : false,
|
||||
hintOptions: {
|
||||
completeOnSingleClick: true,
|
||||
completeSingle: false,
|
||||
|
@ -811,7 +825,13 @@ export class AuthorInput extends React.Component<IAuthorInputProps, {}> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const className = classNames('author-input-component', this.props.className)
|
||||
const className = classNames(
|
||||
'author-input-component',
|
||||
this.props.className,
|
||||
{
|
||||
disabled: this.props.disabled,
|
||||
}
|
||||
)
|
||||
return <div className={className} ref={this.onContainerRef} />
|
||||
}
|
||||
}
|
||||
|
|
|
@ -100,14 +100,14 @@ export class ConfigureGitUser extends React.Component<
|
|||
label="Name"
|
||||
placeholder="Your Name"
|
||||
value={this.state.name}
|
||||
onChange={this.onNameChange}
|
||||
onValueChanged={this.onNameChange}
|
||||
/>
|
||||
|
||||
<TextBox
|
||||
label="Email"
|
||||
placeholder="your-email@example.com"
|
||||
value={this.state.email}
|
||||
onChange={this.onEmailChange}
|
||||
onValueChanged={this.onEmailChange}
|
||||
/>
|
||||
|
||||
<Row>
|
||||
|
@ -131,16 +131,13 @@ export class ConfigureGitUser extends React.Component<
|
|||
)
|
||||
}
|
||||
|
||||
private onNameChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
private onNameChange = (name: string) => {
|
||||
this.setState({
|
||||
name: event.currentTarget.value,
|
||||
email: this.state.email,
|
||||
avatarURL: this.state.avatarURL,
|
||||
name,
|
||||
})
|
||||
}
|
||||
|
||||
private onEmailChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
const email = event.currentTarget.value
|
||||
private onEmailChange = (email: string) => {
|
||||
const avatarURL = this.avatarURLForEmail(email)
|
||||
|
||||
this.setState({
|
||||
|
|
|
@ -165,12 +165,12 @@ export class FilterList<T extends IFilterListItem> extends React.Component<
|
|||
|
||||
<Row className="filter-field-row">
|
||||
<TextBox
|
||||
ref={this.filterTextBoxRef}
|
||||
ref={this.onTextBoxRef}
|
||||
type="search"
|
||||
autoFocus={true}
|
||||
placeholder="Filter"
|
||||
className="filter-list-filter-field"
|
||||
onChange={this.onFilterChanged}
|
||||
onValueChanged={this.onFilterChanged}
|
||||
onKeyDown={this.onKeyDown}
|
||||
value={this.props.filterText}
|
||||
disabled={this.props.disabled}
|
||||
|
@ -184,7 +184,7 @@ export class FilterList<T extends IFilterListItem> extends React.Component<
|
|||
)
|
||||
}
|
||||
|
||||
private filterTextBoxRef = (component: TextBox | null) => {
|
||||
private onTextBoxRef = (component: TextBox | null) => {
|
||||
this.filterTextBox = component
|
||||
}
|
||||
|
||||
|
@ -242,8 +242,7 @@ export class FilterList<T extends IFilterListItem> extends React.Component<
|
|||
this.list = instance
|
||||
}
|
||||
|
||||
private onFilterChanged = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
const text = event.currentTarget.value
|
||||
private onFilterChanged = (text: string) => {
|
||||
if (this.props.onFilterTextChanged) {
|
||||
this.props.onFilterTextChanged(text)
|
||||
}
|
||||
|
|
|
@ -25,9 +25,6 @@ interface ITextBoxProps {
|
|||
/** Whether the input field is disabled. */
|
||||
readonly disabled?: boolean
|
||||
|
||||
/** Called when the user changes the value in the input field. */
|
||||
readonly onChange?: (event: React.FormEvent<HTMLInputElement>) => void
|
||||
|
||||
/**
|
||||
* Called when the user changes the value in the input field.
|
||||
*
|
||||
|
@ -138,14 +135,8 @@ export class TextBox extends React.Component<ITextBoxProps, ITextBoxState> {
|
|||
private onChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
const value = event.currentTarget.value
|
||||
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(event)
|
||||
}
|
||||
|
||||
const defaultPrevented = event.defaultPrevented
|
||||
|
||||
this.setState({ value }, () => {
|
||||
if (this.props.onValueChanged && !defaultPrevented) {
|
||||
if (this.props.onValueChanged) {
|
||||
this.props.onValueChanged(value)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -78,12 +78,12 @@ export class PublishRepository extends React.Component<
|
|||
this.props.onSettingsChanged(newSettings)
|
||||
}
|
||||
|
||||
private onNameChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
this.updateSettings({ name: event.currentTarget.value })
|
||||
private onNameChange = (name: string) => {
|
||||
this.updateSettings({ name })
|
||||
}
|
||||
|
||||
private onDescriptionChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
this.updateSettings({ description: event.currentTarget.value })
|
||||
private onDescriptionChange = (description: string) => {
|
||||
this.updateSettings({ description })
|
||||
}
|
||||
|
||||
private onPrivateChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
|
@ -146,7 +146,7 @@ export class PublishRepository extends React.Component<
|
|||
label="Name"
|
||||
value={this.props.settings.name}
|
||||
autoFocus={true}
|
||||
onChange={this.onNameChange}
|
||||
onValueChanged={this.onNameChange}
|
||||
/>
|
||||
</Row>
|
||||
|
||||
|
@ -154,7 +154,7 @@ export class PublishRepository extends React.Component<
|
|||
<TextBox
|
||||
label="Description"
|
||||
value={this.props.settings.description}
|
||||
onChange={this.onDescriptionChange}
|
||||
onValueChanged={this.onDescriptionChange}
|
||||
/>
|
||||
</Row>
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ export class RenameBranch extends React.Component<
|
|||
label="Name"
|
||||
autoFocus={true}
|
||||
value={this.state.newName}
|
||||
onChange={this.onNameChange}
|
||||
onValueChanged={this.onNameChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
/>
|
||||
</Row>
|
||||
|
@ -75,8 +75,8 @@ export class RenameBranch extends React.Component<
|
|||
}
|
||||
}
|
||||
|
||||
private onNameChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
this.setState({ newName: event.currentTarget.value })
|
||||
private onNameChange = (name: string) => {
|
||||
this.setState({ newName: name })
|
||||
}
|
||||
|
||||
private cancel = () => {
|
||||
|
|
|
@ -14,18 +14,23 @@
|
|||
padding: 0 var(--spacing-half);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--focus-color);
|
||||
box-shadow: 0 0 0 1px var(--text-field-focus-shadow-color);
|
||||
@include textboxish-focus-styles;
|
||||
}
|
||||
}
|
||||
|
||||
// This is kept separate from textboxish because we need to be able
|
||||
// to apply it conditionally when running on Windows, the disabled
|
||||
// styles on macOS are fine.
|
||||
@mixin textboxish-focus-styles {
|
||||
outline: none;
|
||||
border-color: var(--focus-color);
|
||||
box-shadow: 0 0 0 1px var(--text-field-focus-shadow-color);
|
||||
}
|
||||
|
||||
@mixin textboxish-disabled-styles {
|
||||
background: var(--box-alt-background-color);
|
||||
color: var(--text-secondary-color);
|
||||
}
|
||||
|
||||
@mixin textboxish-disabled {
|
||||
&:disabled {
|
||||
background: var(--box-alt-background-color);
|
||||
color: var(--text-secondary-color);
|
||||
@include textboxish-disabled-styles;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
@import '../mixins';
|
||||
|
||||
.author-input-component {
|
||||
&.disabled .CodeMirror {
|
||||
@include textboxish-disabled-styles;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
border: 1px solid var(--box-border-color);
|
||||
border-radius: var(--border-radius);
|
||||
font-size: var(--font-size);
|
||||
font-family: var(--font-family-sans-serif);
|
||||
@include textboxish;
|
||||
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-top: 0;
|
||||
|
@ -53,9 +57,7 @@
|
|||
}
|
||||
|
||||
.CodeMirror-focused {
|
||||
outline: none;
|
||||
border-color: var(--focus-color);
|
||||
box-shadow: 0 0 0 1px var(--text-field-focus-shadow-color);
|
||||
@include textboxish-focus-styles;
|
||||
border-top-width: 1px;
|
||||
border-top-style: solid;
|
||||
margin-top: -1px;
|
||||
|
|
|
@ -1,28 +1,16 @@
|
|||
@import '../mixins';
|
||||
|
||||
.text-area-component {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
textarea {
|
||||
border: 1px solid var(--box-border-color);
|
||||
border-radius: var(--border-radius);
|
||||
color: currentColor;
|
||||
font-size: var(--font-size);
|
||||
font-family: var(--font-family-sans-serif);
|
||||
padding: var(--spacing-half);
|
||||
@include textboxish;
|
||||
@include textboxish-disabled;
|
||||
|
||||
height: auto;
|
||||
resize: none;
|
||||
|
||||
min-height: 100px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--focus-color);
|
||||
box-shadow: 0 0 0 1px var(--text-field-focus-shadow-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,10 +26,6 @@
|
|||
|
||||
input {
|
||||
@include textboxish;
|
||||
|
||||
// Don't override disabled styles on darwin for text boxes.
|
||||
@include win32 {
|
||||
@include textboxish-disabled;
|
||||
}
|
||||
@include textboxish-disabled;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -119,7 +119,6 @@
|
|||
|
||||
.action-bar {
|
||||
padding: var(--spacing-half);
|
||||
display: inline-block;
|
||||
|
||||
// We're not faking being a textbox any more
|
||||
cursor: default;
|
||||
|
@ -129,9 +128,23 @@
|
|||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: var(--box-alt-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.co-authors-toggle {
|
||||
// button reset styles
|
||||
border: none;
|
||||
-webkit-appearance: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
align-items: center;
|
||||
display: block;
|
||||
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
cursor: pointer;
|
||||
|
@ -148,6 +161,10 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:after {
|
||||
opacity: 1;
|
||||
|
|
|
@ -1,5 +1,37 @@
|
|||
{
|
||||
"releases": {
|
||||
"1.1.1-beta1": [
|
||||
|
||||
]
|
||||
,"1.1.0": [
|
||||
"[New] Check out pull requests from collaborators or forks from within Desktop",
|
||||
"[New] View the commit status of the branch when it has an open pull request",
|
||||
"[Added] Add RubyMine support for macOS - #3883. Thanks @gssbzn!",
|
||||
"[Added] Add TextMate support for macOS - #3910. Thanks @caiofbpa!",
|
||||
"[Added] Syntax highlighting for Elixir files - #3774. Thanks @joaovitoras!",
|
||||
"[Fixed] Update layout of branch blankslate image - #4011",
|
||||
"[Fixed] Expanded avatar stack in commit summary gets cut off - #3884",
|
||||
"[Fixed] Clear repository filter when switching tabs - #3787. Thanks @reyronald!",
|
||||
"[Fixed] Avoid crash when unable to launch shell - #3954",
|
||||
"[Fixed] Ensure renames are detected when viewing commit diffs - #3673",
|
||||
"[Fixed] Fetch default remote if it differs from the current - #4056",
|
||||
"[Fixed] Handle Git errors when .gitmodules are malformed - #3912",
|
||||
"[Fixed] Handle error when \"where\" is not on PATH - #3882 #3825",
|
||||
"[Fixed] Ignore action assumes CRLF when core.autocrlf is unset - #3514",
|
||||
"[Fixed] Prevent duplicate entries in co-author autocomplete list - #3887",
|
||||
"[Fixed] Renames not detected when viewing commit diffs - #3673",
|
||||
"[Fixed] Support legacy usernames as co-authors - #3897",
|
||||
"[Improved] Update branch button text from \"New\" to \"New Branch\" - #4032",
|
||||
"[Improved] Add fuzzy search in the repository, branch, PR, and clone FilterLists - #911. Thanks @j-f1!",
|
||||
"[Improved] Tidy up commit summary and description layout in commit list - #3922. Thanks @willnode!",
|
||||
"[Improved] Use smaller default size when rendering Gravatar avatars - #3911",
|
||||
"[Improved] Show fetch progress when initializing remote for fork - #3953",
|
||||
"[Improved] Remove references to Hubot from the user setup page - #4015. Thanks @j-f1!",
|
||||
"[Improved] Error handling around ENOENT - #3954",
|
||||
"[Improved] Clear repository filter text when switching tabs - #3787. Thanks @reyronald!",
|
||||
"[Improved] Allow window to accept single click on focus - #3843",
|
||||
"[Improved] Disable drag-and-drop interaction when a popup is in the foreground - #3996"
|
||||
],
|
||||
"1.1.0-beta3": [
|
||||
"[Fixed] Fetch default remote if it differs from the current - #4056"
|
||||
],
|
||||
|
|
|
@ -8,7 +8,7 @@ documentation.
|
|||
If you are interested in contributing to the project, you should read these
|
||||
resources to get familiar with how things work:
|
||||
|
||||
- **[How Can I Contribute?](../CONTRIBUTING.md#how-can-i-contribute)** -
|
||||
- **[How Can I Contribute?](../.github/CONTRIBUTING.md#how-can-i-contribute)** -
|
||||
details about how you can participate
|
||||
- **[Development Environment Setup](contributing/setup.md)** - everything
|
||||
you need to know to get Desktop up and running
|
||||
|
|
|
@ -182,7 +182,7 @@ require('devtron').install()
|
|||
|
||||
You're almost there! Here's a couple of things we recommend you read next:
|
||||
|
||||
- [Help Wanted](../../CONTRIBUTING.md#help-wanted) - we've marked some tasks in
|
||||
- [Help Wanted](../../.github/CONTRIBUTING.md#help-wanted) - we've marked some tasks in
|
||||
the backlog that are ideal for external contributors
|
||||
- [Code Reviews](../process/reviews.md) - some notes on how the team does
|
||||
code reviews
|
||||
|
|
104
docs/known-issues.md
Normal file
104
docs/known-issues.md
Normal file
|
@ -0,0 +1,104 @@
|
|||
# Known Issues
|
||||
|
||||
This document outlines acknowledged issues with GitHub Desktop, including workarounds if known.
|
||||
|
||||
## What should I do if...
|
||||
|
||||
### I have encountered an issue listed here?
|
||||
|
||||
Some known issues have a workaround that users have reported addresses the issue. Please try the workaround for yourself to confirm it addresses the issue.
|
||||
|
||||
### I have additional questions about an issue listed here?
|
||||
|
||||
Each known issue links off to an existing GitHub issue. If you have additional questions or feedback, please comment on the issue.
|
||||
|
||||
### My issue is not listed here?
|
||||
|
||||
Please check the [open](https://github.com/desktop/desktop/labels/bug) and [closed](https://github.com/desktop/desktop/issues?q=is%3Aclosed+label%3Abug) bugs in the issue tracker for the details of your bug. If you can't find it, or if you're not sure, open a [new issue](https://github.com/desktop/desktop/issues/new).
|
||||
|
||||
## macOS
|
||||
|
||||
### 'The username or passphrase you entered is not correct' error after signing into account - [#3263](https://github.com/desktop/desktop/issues/3263)
|
||||
|
||||
This seems to be caused by the Keychain being in an invalid state, affecting applications that try to use the keychain to store or retrieve credentials. Seems to be specific to macOS High Sierra (10.13).
|
||||
|
||||
**Workaround:**
|
||||
|
||||
- Open `Keychain Access.app`
|
||||
- Right-click on the `login` keychain and try locking it
|
||||
- Right-click on the `login` keychain and try unlocking it
|
||||
- Sign into your GitHub account again
|
||||
|
||||
## Windows
|
||||
|
||||
### Window is hidden after detaching secondary monitor - [#2107](https://github.com/desktop/desktop/issues/2107)
|
||||
|
||||
This is related to Desktop tracking the window position between launches, but not changes to your display configuration such as removing the secondary monitor where Desktop was positioned.
|
||||
|
||||
**Workaround:**
|
||||
|
||||
- Remove `%APPDATA%\GitHub Desktop\window-state.json`
|
||||
- Restart Desktop
|
||||
|
||||
### Certificate revocation check fails - [#3326](https://github.com/desktop/desktop/issues/3326)
|
||||
|
||||
If you are using Desktop on a corporate network, you may encounter an error like this:
|
||||
|
||||
```
|
||||
fatal: unable to access 'https://github.com/owner/name.git/': schannel: next InitializeSecurityContext failed: Unknown error (0x80092012) - The revocation function was unable to check revocation for the certificate.
|
||||
```
|
||||
|
||||
GitHub Desktop by default uses the Windows Secure Channel (SChannel) APIs to validate the certificate received from a server. Some networks will block the attempts by Windows to check the revocation status of a certificate, which then causes the whole operation to error.
|
||||
|
||||
**Workaround:**
|
||||
|
||||
To use the classic OpenSSL behavior in Git, you'll need a PEM file containing certificates that are considered trusted. The [public list](https://curl.haxx.se/docs/caextract.html) provided by the curl project can be used if you are not connecting to a GitHub Enterprise instance which has it's own distinct certificates.
|
||||
|
||||
Once you've downloaded that PEM file somewhere, open a shell with Git and run these commands:
|
||||
|
||||
```shellsession
|
||||
$ git config --global http.sslBackend "openssl"
|
||||
$ git config --global http.sslCAInfo [path to .pem file]
|
||||
```
|
||||
|
||||
### Using a repository configured with Folder Redirection - [#2972](https://github.com/desktop/desktop/issues/2972)
|
||||
|
||||
[Folder Redirection](https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/cc753996(v%3dws.11)) is an feature of Windows for administrators to ensure files and folders are managed on a network server, instead.
|
||||
|
||||
**Not supported** as Git is not able to resolve the working directory correctly:
|
||||
|
||||
```shellsession
|
||||
2017-09-21T23:16:05.933Z - error: [ui] `git -c credential.helper= lfs clone --recursive --progress --progress -- https://github.com/owner/name.git \\harvest\Redirected\andrewd\My Documents\GitHub\name` exited with an unexpected code: 2.
|
||||
Cloning into '\\harvest\Redirected\andrewd\My Documents\GitHub\name'...
|
||||
remote: Counting objects: 4, done.
|
||||
remote: Compressing objects: 33% (1/3)
|
||||
remote: Compressing objects: 66% (2/3)
|
||||
remote: Compressing objects: 100% (3/3)
|
||||
remote: Compressing objects: 100% (3/3), done.
|
||||
remote: Total 4 (delta 1), reused 4 (delta 1), pack-reused 0
|
||||
fatal: unable to get current working directory: No such file or directory
|
||||
warning: Clone succeeded, but checkout failed.
|
||||
You can inspect what was checked out with 'git status'
|
||||
and retry the checkout with 'git checkout -f HEAD'
|
||||
|
||||
Error(s) during clone:
|
||||
git clone failed: exit status 128
|
||||
```
|
||||
|
||||
### Enable Mandatory ASLR triggers cygheap errors - #3096
|
||||
|
||||
Windows 10 Fall Creators Edition (version 1709 or later) added enhancements to the Enhanced Mitigation Experience Toolkit, one being to enable Mandatory ASLR. This setting affects the embedded Git shipped in Desktop, and produces errors that look like this:
|
||||
|
||||
```
|
||||
1 [main] sh (2072) C:\Users\bdorrans\AppData\Local\GitHubDesktop\app-1.0.4\resources\app\git\usr\bin\sh.exe: *** fatal error - cygheap base mismatch detected - 0x2E07408/0x2EC7408.
|
||||
This problem is probably due to using incompatible versions of the cygwin DLL.
|
||||
Search for cygwin1.dll using the Windows Start->Find/Search facility
|
||||
and delete all but the most recent version. The most recent version *should*
|
||||
reside in x:\cygwin\bin, where 'x' is the drive on which you have
|
||||
installed the cygwin distribution. Rebooting is also suggested if you
|
||||
are unable to find another cygwin DLL.
|
||||
```
|
||||
|
||||
Enabling Mandatory ASLR affects the MSYS2 core library, which is relied upon by Git for Windows to emulate process forking.
|
||||
|
||||
**Not supported:** this is an upstream limitation of MSYS2, and it is recommend that you either disable Mandatory ASLR or whitelist all executables under `<Git>\usr\bin` which depend on MSYS2.
|
|
@ -81,16 +81,16 @@ issues from time to time that isn't and won't be covered here.
|
|||
|
||||
1. Person files a new issue
|
||||
1. Maintainer checks to ensure they adequately filled out the template. If not,
|
||||
close with the [request to fill out the template](canned-messages/needs-template.md).
|
||||
close with a request to fill out the template.
|
||||
1. Label the issue as a `bug` if the issue is a regression or behaviour that
|
||||
needs to be fixed.
|
||||
1. If the issue has already been fixed, add a comment linking to the original
|
||||
issue and close the issue.
|
||||
1. If anything is unclear but the template is adequately filled out, post what
|
||||
questions you have and label with `more-information-needed`
|
||||
questions you have and label with `more-information-needed`.
|
||||
1. Maintainer attempts to reproduce the problem
|
||||
1. If the problem is not reproducible, label with `needs-reproduction` and
|
||||
ask the author of the issue for [clarification on the repro steps](canned-messages/repro-steps.md)
|
||||
ask the author of the issue for clarification on the repro steps.
|
||||
1. Label the issue as an `enhancement` if the issue mentions new behaviour
|
||||
or functionality that the app should have.
|
||||
|
||||
|
@ -101,7 +101,8 @@ issues from time to time that isn't and won't be covered here.
|
|||
Periodically we should be doing a sweep of issues that are open and labeled
|
||||
`more-information-needed`. If the original poster has not responded within
|
||||
two weeks after the last question by an official maintainer, close the issue
|
||||
with [the no response message](canned-messages/no-response.md).
|
||||
with a message saying that you're closing the issue due to inactivity but are
|
||||
happy to re-open if they can provide more details.
|
||||
|
||||
## Needs Reproduction
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# Editor Integration
|
||||
# "Open External Editor" integration
|
||||
|
||||
GitHub Desktop supports the user choosing an external program to open their
|
||||
local repositories, and this is available from the main menu and right-clicking
|
||||
on a repository in the sidebar.
|
||||
local repositories, and this is available from the top-level **Repository** menu
|
||||
or when right-clicking on a repository in the sidebar.
|
||||
|
||||
### My favourite editor XYZ isn't supported!
|
||||
|
||||
|
@ -25,8 +25,9 @@ The source for the editor integration on Windows is found in
|
|||
These editors are currently supported:
|
||||
|
||||
- [Atom](https://atom.io/)
|
||||
- [Visual Studio Code](https://code.visualstudio.com/)
|
||||
- [Visual Studio Code](https://code.visualstudio.com/) - both stable and Insiders channel
|
||||
- [Sublime Text](https://www.sublimetext.com/)
|
||||
- [ColdFusion Builder](https://www.adobe.com/products/coldfusion-builder.html)
|
||||
|
||||
These are defined in an enum at the top of the file:
|
||||
|
||||
|
@ -34,7 +35,9 @@ These are defined in an enum at the top of the file:
|
|||
export enum ExternalEditor {
|
||||
Atom = 'Atom',
|
||||
VisualStudioCode = 'Visual Studio Code',
|
||||
VisualStudioCodeInsiders = 'Visual Studio Code (Insiders)',
|
||||
SublimeText = 'Sublime Text',
|
||||
CFBuilder = 'ColdFusion Builder',
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -202,7 +205,7 @@ The source for the editor integration on macOS is found in
|
|||
These editors are currently supported:
|
||||
|
||||
- [Atom](https://atom.io/)
|
||||
- [Visual Studio Code](https://code.visualstudio.com/)
|
||||
- [Visual Studio Code](https://code.visualstudio.com/) - both stable and Insiders channel
|
||||
- [Sublime Text](https://www.sublimetext.com/)
|
||||
- [BBEdit](http://www.barebones.com/products/bbedit/)
|
||||
- [PhpStorm](https://www.jetbrains.com/phpstorm/)
|
||||
|
@ -212,10 +215,16 @@ These editors are currently supported:
|
|||
These are defined in an enum at the top of the file:
|
||||
|
||||
```ts
|
||||
|
||||
export enum ExternalEditor {
|
||||
Atom = 'Atom',
|
||||
VisualStudioCode = 'Visual Studio Code',
|
||||
VisualStudioCodeInsiders = 'Visual Studio Code (Insiders)',
|
||||
SublimeText = 'Sublime Text',
|
||||
BBEdit = 'BBEdit',
|
||||
PhpStorm = 'PhpStorm',
|
||||
RubyMine = 'RubyMine',
|
||||
TextMate = 'TextMate',
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -297,7 +306,7 @@ The source for the editor integration on Linux is found in
|
|||
These editors are currently supported:
|
||||
|
||||
- [Atom](https://atom.io/)
|
||||
- [Visual Studio Code](https://code.visualstudio.com/)
|
||||
- [Visual Studio Code](https://code.visualstudio.com/) - both stable and Insiders channel
|
||||
- [Sublime Text](https://www.sublimetext.com/)
|
||||
|
||||
These are defined in an enum at the top of the file:
|
||||
|
@ -306,6 +315,7 @@ These are defined in an enum at the top of the file:
|
|||
export enum ExternalEditor {
|
||||
Atom = 'Atom',
|
||||
VisualStudioCode = 'Visual Studio Code',
|
||||
VisualStudioCodeInsiders = 'Visual Studio Code (Insiders)',
|
||||
SublimeText = 'Sublime Text',
|
||||
}
|
||||
```
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Shell Integration
|
||||
# "Open Shell" integration
|
||||
|
||||
GitHub Desktop supports launching an available shell found on the user's
|
||||
machine, to work with Git repositories outside of Desktop.
|
||||
|
@ -46,22 +46,50 @@ use **Git Bash** as a reference for the rest of the process.
|
|||
### Step 1: Find the shell executable
|
||||
|
||||
The `getAvailableShells()` method is used to find each shell, as some may not
|
||||
be present on the user's machine. You will need to add some code in here.
|
||||
be present on the user's machine.
|
||||
|
||||
For Git Bash we perform a couple of checks:
|
||||
This is the example for how we resolve if Git Bash is installed.
|
||||
|
||||
```ts
|
||||
const gitBash = await readRegistryKeySafe(
|
||||
'HKEY_LOCAL_MACHINE\\SOFTWARE\\GitForWindows'
|
||||
)
|
||||
if (gitBash.length > 0) {
|
||||
const installPathEntry = gitBash.find(e => e.name === 'InstallPath')
|
||||
if (installPathEntry) {
|
||||
const gitBashPath = await findGitBash()
|
||||
if (gitBashPath != null) {
|
||||
shells.push({
|
||||
shell: Shell.GitBash,
|
||||
path: Path.join(installPathEntry.value, 'git-bash.exe'),
|
||||
path: gitBashPath,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
You will need to add some code in here, following this pattern, to resolve a new shell.
|
||||
|
||||
The details of this check will vary based on the how the shell is installed,
|
||||
but Git Bash is a good example of this:
|
||||
|
||||
```ts
|
||||
async function findGitBash(): Promise<string | null> {
|
||||
const registryPath = enumerateValues(
|
||||
HKEY.HKEY_LOCAL_MACHINE,
|
||||
'SOFTWARE\\GitForWindows'
|
||||
)
|
||||
|
||||
if (registryPath.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const installPathEntry = registryPath.find(e => e.name === 'InstallPath')
|
||||
if (installPathEntry && installPathEntry.type === RegistryValueType.REG_SZ) {
|
||||
const path = Path.join(installPathEntry.data, 'git-bash.exe')
|
||||
|
||||
if (await pathExists(path)) {
|
||||
return path
|
||||
} else {
|
||||
log.debug(
|
||||
`[Git Bash] registry entry found but does not exist at '${path}'`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -75,14 +103,16 @@ This approximately reads as:
|
|||
|
||||
The `launch()` function defines the arguments to pass to the shell, and each
|
||||
shell may require it's own set of command arguments. You will need to make
|
||||
changes here.
|
||||
changes here to handle a new shell.
|
||||
|
||||
```ts
|
||||
} else if (shell === Shell.GitBash) {
|
||||
await spawn(foundShell.path, [`--cd="${path}"`], {
|
||||
shell: true,
|
||||
cwd: path,
|
||||
})
|
||||
case Shell.GitBash:
|
||||
const gitBashPath = `"${foundShell.path}"`
|
||||
log.info(`launching ${shell} at path: ${gitBashPath}`)
|
||||
return spawn(gitBashPath, [`--cd="${path}"`], {
|
||||
shell: true,
|
||||
cwd: path,
|
||||
})
|
||||
```
|
||||
|
||||
## macOS
|
||||
|
@ -152,10 +182,13 @@ at the path requested by the user. You may not need to make changes here,
|
|||
unless your shell behaviour differs significantly from this.
|
||||
|
||||
```ts
|
||||
export async function launch(shell: Shell, path: string): Promise<void> {
|
||||
const bundleID = getBundleID(shell)
|
||||
export function launch(
|
||||
foundShell: IFoundShell<Shell>,
|
||||
path: string
|
||||
): ChildProcess {
|
||||
const bundleID = getBundleID(foundShell.shell)
|
||||
const commandArgs = ['-b', bundleID, path]
|
||||
await spawn('open', commandArgs)
|
||||
return spawn('open', commandArgs)
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -223,33 +256,28 @@ export async function getAvailableShells(): Promise<
|
|||
|
||||
### Step 2: Launch the shell
|
||||
|
||||
The `launch()` method will use the received `shell.shell` executable path and
|
||||
the path requested by the user. You may need to make changes here, if your
|
||||
shell has a different command line interface.
|
||||
The `launch()` method will use the received `foundShell` executable path and
|
||||
the path requested by the user. You will need to make changes here, to ensure
|
||||
the correct arguments are passed to the command line interface:
|
||||
|
||||
```ts
|
||||
export async function launch(
|
||||
shell: IFoundShell<Shell>,
|
||||
export function launch(
|
||||
foundShell: IFoundShell<Shell>,
|
||||
path: string
|
||||
): Promise<void> {
|
||||
if (shell.shell === Shell.Urxvt) {
|
||||
const commandArgs = ['-cd', path]
|
||||
await spawn(shell.path, commandArgs)
|
||||
): ChildProcess {
|
||||
const shell = foundShell.shell
|
||||
switch (shell) {
|
||||
case Shell.Urxvt:
|
||||
return spawn(foundShell.path, ['-cd', path])
|
||||
case Shell.Konsole:
|
||||
return spawn(foundShell.path, ['--workdir', path])
|
||||
case Shell.Xterm:
|
||||
return spawn(foundShell.path, ['-e', '/bin/bash'], { cwd: path })
|
||||
case Shell.Tilix:
|
||||
case Shell.Gnome:
|
||||
return spawn(foundShell.path, ['--working-directory', path])
|
||||
default:
|
||||
return assertNever(shell, `Unknown shell: ${shell}`)
|
||||
}
|
||||
|
||||
if (shell.shell === Shell.Konsole) {
|
||||
const commandArgs = ['--workdir', path]
|
||||
await spawn(shell.path, commandArgs)
|
||||
}
|
||||
|
||||
if (shell.shell === Shell.Xterm) {
|
||||
const commandArgs = ['-e', '/bin/bash']
|
||||
const commandOptions = { cwd: path }
|
||||
await spawn(shell.path, commandArgs, commandOptions)
|
||||
}
|
||||
|
||||
const commandArgs = ['--working-directory', path]
|
||||
await spawn(shell.path, commandArgs)
|
||||
}
|
||||
|
||||
```
|
||||
|
|
|
@ -16,7 +16,7 @@ Note, however that this list is likely to grow stale so I'd recommend checking [
|
|||
|
||||
### I want to add my favorite language
|
||||
|
||||
Cool! As long as it's a language that [CodeMirror supports out of the box](https://codemirror.net/mode/index.html) we should be able to make it work. Open an issue and we'll take it from there.
|
||||
Cool! As long as it's a language that [CodeMirror supports out of the box](https://codemirror.net/mode/index.html) or it has a module that extends CodeMirror ([`codemirror-mode-elixir`](https://github.com/optick/codemirror-mode-elixir) is an example of this) we should be able to make it work. Open an issue and we'll take it from there.
|
||||
|
||||
If you want to create a PR and add highlighter support for your favourite programming language don't forget to:
|
||||
1. Submit a PR with a sample file for the language to [desktop/highlighter-tests](https://github.com/desktop/highlighter-tests).
|
||||
|
|
Loading…
Reference in a new issue