Merge branch 'development' into releases/1.6.5

This commit is contained in:
evelyn masso 2019-03-20 09:43:39 -07:00
commit 9f1167bace
84 changed files with 2097 additions and 913 deletions

View file

@ -1,7 +1,7 @@
root: true
parser: '@typescript-eslint/parser'
plugins:
- typescript
- '@typescript-eslint'
- babel
- react
- json
@ -20,23 +20,23 @@ rules:
# PLUGINS #
###########
# TYPESCRIPT
typescript/interface-name-prefix:
'@typescript-eslint/interface-name-prefix':
- error
- always
typescript/no-angle-bracket-type-assertion: error
typescript/explicit-member-accessibility: error
typescript/no-unused-vars: error
typescript/no-use-before-define:
'@typescript-eslint/no-angle-bracket-type-assertion': error
'@typescript-eslint/explicit-member-accessibility': error
'@typescript-eslint/no-unused-vars':
- error
- args: 'none'
'@typescript-eslint/no-use-before-define':
- error
- functions: false
variables: false
typedefs: false
## blocked by https://github.com/nzakas/eslint-plugin-typescript/pull/23
# typescript/member-ordering: error
##
## blocked by https://github.com/nzakas/eslint-plugin-typescript/issues/41
# typescript/type-annotation-spacing: error
##
# this rule now works but generates a lot of issues with the codebase
# '@typescript-eslint/member-ordering': error
'@typescript-eslint/type-annotation-spacing': error
# Babel
babel/no-invalid-this: error
@ -48,6 +48,8 @@ rules:
react/jsx-key: error
react/jsx-no-bind: error
react/no-string-refs: error
react/jsx-uses-vars: error
react/jsx-uses-react: error
###########
# BUILTIN #

View file

@ -21,12 +21,12 @@
"byline": "^5.0.0",
"chalk": "^2.3.0",
"classnames": "^2.2.5",
"codemirror": "^5.31.0",
"codemirror-mode-elixir": "1.1.1",
"codemirror": "^5.44.0",
"codemirror-mode-elixir": "^1.1.2",
"deep-equal": "^1.0.1",
"dexie": "^2.0.0",
"double-ended-queue": "^2.1.0-0",
"dugite": "1.80.0",
"dugite": "1.85.0",
"electron-window-state": "^4.0.2",
"event-kit": "^2.0.0",
"file-uri-to-path": "0.0.2",
@ -34,9 +34,9 @@
"fs-admin": "^0.1.7",
"fs-extra": "^6.0.0",
"fuzzaldrin-plus": "^0.6.0",
"keytar": "^4.3.0",
"keytar": "^4.4.1",
"memoize-one": "^4.0.3",
"moment": "^2.17.1",
"moment": "^2.24.0",
"mri": "^1.1.0",
"primer-support": "^4.0.0",
"queue": "^4.4.2",

View file

@ -1,4 +1,4 @@
/* eslint-disable typescript/interface-name-prefix */
/* eslint-disable @typescript-eslint/interface-name-prefix */
declare namespace CodeMirror {
interface EditorConfiguration {

View file

@ -25,6 +25,8 @@ if (!ClientID || !ClientID.length || !ClientSecret || !ClientSecret.length) {
)
}
type GitHubAccountType = 'User' | 'Organization'
/** The OAuth scopes we need. */
const Scopes = ['repo', 'user']
@ -44,7 +46,7 @@ export interface IAPIRepository {
readonly ssh_url: string
readonly html_url: string
readonly name: string
readonly owner: IAPIUser
readonly owner: IAPIIdentity
readonly private: boolean
readonly fork: boolean
readonly default_branch: string
@ -57,13 +59,42 @@ export interface IAPIRepository {
*/
export interface IAPICommit {
readonly sha: string
readonly author: IAPIUser | null
readonly author: IAPIIdentity | {} | null
}
/**
* Information about a user as returned by the GitHub API.
* Entity returned by the `/user/orgs` endpoint.
*
* Because this is specific to one endpoint it omits the `type` member from
* `IAPIIdentity` that callers might expect.
*/
export interface IAPIUser {
export interface IAPIOrganization {
readonly id: number
readonly url: string
readonly login: string
readonly avatar_url: string
}
/**
* Minimum subset of an identity returned by the GitHub API
*/
export interface IAPIIdentity {
readonly id: number
readonly url: string
readonly login: string
readonly avatar_url: string
readonly type: GitHubAccountType
}
/**
* Complete identity details returned in some situations by the GitHub API.
*
* If you are not sure what is returned as part of an API response, you should
* use `IAPIIdentity` as that contains the known subset of an identity and does
* not cover scenarios where privacy settings of a user control what information
* is returned.
*/
interface IAPIFullIdentity {
readonly id: number
readonly url: string
readonly login: string
@ -80,7 +111,7 @@ export interface IAPIUser {
* specified a public email address in their profile.
*/
readonly email: string | null
readonly type: 'User' | 'Organization'
readonly type: GitHubAccountType
}
/** The users we get from the mentionables endpoint. */
@ -161,7 +192,7 @@ export interface IAPIPullRequest {
readonly number: number
readonly title: string
readonly created_at: string
readonly user: IAPIUser
readonly user: IAPIIdentity
readonly head: IAPIPullRequestRef
readonly base: IAPIPullRequestRef
}
@ -283,10 +314,10 @@ export class API {
}
/** Fetch the logged in account. */
public async fetchAccount(): Promise<IAPIUser> {
public async fetchAccount(): Promise<IAPIFullIdentity> {
try {
const response = await this.request('GET', 'user')
const result = await parsedResponse<IAPIUser>(response)
const result = await parsedResponse<IAPIFullIdentity>(response)
return result
} catch (e) {
log.warn(`fetchAccount: failed with endpoint ${this.endpoint}`, e)
@ -328,12 +359,20 @@ export class API {
}
/** Search for a user with the given public email. */
public async searchForUserWithEmail(email: string): Promise<IAPIUser | null> {
public async searchForUserWithEmail(
email: string
): Promise<IAPIIdentity | null> {
if (email.length === 0) {
return null
}
try {
const params = { q: `${email} in:email type:user` }
const url = urlWithQueryString('search/users', params)
const response = await this.request('GET', url)
const result = await parsedResponse<ISearchResults<IAPIUser>>(response)
const result = await parsedResponse<ISearchResults<IAPIIdentity>>(
response
)
const items = result.items
if (items.length) {
// The results are sorted by score, best to worst. So the first result
@ -349,9 +388,9 @@ export class API {
}
/** Fetch all the orgs to which the user belongs. */
public async fetchOrgs(): Promise<ReadonlyArray<IAPIUser>> {
public async fetchOrgs(): Promise<ReadonlyArray<IAPIOrganization>> {
try {
return this.fetchAll<IAPIUser>('user/orgs')
return this.fetchAll<IAPIOrganization>('user/orgs')
} catch (e) {
log.warn(`fetchOrgs: failed with endpoint ${this.endpoint}`, e)
return []
@ -360,7 +399,7 @@ export class API {
/** Create a new GitHub repository with the given properties. */
public async createRepository(
org: IAPIUser | null,
org: IAPIOrganization | null,
name: string,
description: string,
private_: boolean
@ -569,7 +608,7 @@ export class API {
* Retrieve the public profile information of a user with
* a given username.
*/
public async fetchUser(login: string): Promise<IAPIUser | null> {
public async fetchUser(login: string): Promise<IAPIFullIdentity | null> {
try {
const response = await this.request(
'GET',
@ -580,7 +619,7 @@ export class API {
return null
}
return await parsedResponse<IAPIUser>(response)
return await parsedResponse<IAPIFullIdentity>(response)
} catch (e) {
log.warn(`fetchUser: failed with endpoint ${this.endpoint}`, e)
throw e

View file

@ -162,6 +162,9 @@ export interface IAppState {
/** Whether we should show a confirmation dialog */
readonly askForConfirmationOnDiscardChanges: boolean
/** Should the app prompt the user to confirm a force push? */
readonly askForConfirmationOnForcePush: boolean
/** The external editor to use when opening repositories */
readonly selectedExternalEditor?: ExternalEditor
@ -437,6 +440,9 @@ export interface IBranchesState {
* that the default Git behaviour will occur.
*/
readonly pullWithRebase?: boolean
/** Tracking branches that have been rebased within Desktop */
readonly rebasedBranches: ReadonlyMap<string, string>
}
export interface ICommitSelection {

View file

@ -75,3 +75,8 @@ export function enablePullWithRebase(): boolean {
export function enableGroupRepositoriesByOwner(): boolean {
return enableBetaFeatures()
}
/** Should the app show the "rebase current branch" dialog? */
export function enableRebaseDialog(): boolean {
return enableDevelopmentFeatures()
}

View file

@ -44,7 +44,7 @@ export interface IGitExecutionOptions extends DugiteExecutionOptions {
*/
export interface IGitResult extends DugiteResult {
/**
* The parsed git error. This will be null when the exit code is include in
* The parsed git error. This will be null when the exit code is included in
* the `successExitCodes`, or when dugite was unable to parse the
* error.
*/
@ -264,6 +264,12 @@ function getDescriptionForError(error: DugiteError): string {
return 'A lock file already exists in the repository, which blocks this operation from completing.'
case DugiteError.NoMergeToAbort:
return 'There is no merge in progress, so there is nothing to abort.'
case DugiteError.NoExistingRemoteBranch:
return 'The remote branch does not exist.'
case DugiteError.LocalChangesOverwritten:
return 'Some of your changes would be overwritten.'
case DugiteError.UnresolvedConflicts:
return 'There are unresolved conflicts in the working directory.'
default:
return assertNever(error, `Unknown error: ${error}`)
}

View file

@ -1,3 +1,5 @@
import { GitError as DugiteError } from 'dugite'
import {
git,
IGitExecutionOptions,
@ -10,6 +12,10 @@ import { IGitAccount } from '../../models/git-account'
import { PushProgressParser, executionOptionsWithProgress } from '../progress'
import { envForAuthentication, AuthenticationErrors } from './authentication'
export type PushOptions = {
readonly forceWithLease: boolean
}
/**
* Push from the remote to the branch, optionally setting the upstream.
*
@ -38,6 +44,7 @@ export async function push(
remote: string,
localBranch: string,
remoteBranch: string | null,
options?: PushOptions,
progressCallback?: (progress: IPushProgress) => void
): Promise<void> {
const networkArguments = await gitNetworkArguments(repository, account)
@ -51,11 +58,16 @@ export async function push(
if (!remoteBranch) {
args.push('--set-upstream')
} else if (options !== undefined && options.forceWithLease) {
args.push('--force-with-lease')
}
const expectedErrors = new Set<DugiteError>(AuthenticationErrors)
expectedErrors.add(DugiteError.ProtectedBranchForcePush)
let opts: IGitExecutionOptions = {
env: envForAuthentication(account),
expectedErrors: AuthenticationErrors,
expectedErrors,
}
if (progressCallback) {

View file

@ -1,8 +1,11 @@
import * as Path from 'path'
import { ChildProcess } from 'child_process'
import * as FSE from 'fs-extra'
import { GitError } from 'dugite'
import * as byline from 'byline'
import { Repository } from '../../models/repository'
import { git } from './core'
import { git, IGitResult, IGitExecutionOptions } from './core'
import {
WorkingDirectoryFileChange,
AppFileStatusKind,
@ -10,9 +13,11 @@ import {
import { ManualConflictResolution } from '../../models/manual-conflict-resolution'
import { stageManualConflictResolution } from './stage'
import { stageFiles } from './update-index'
import { GitError, IGitResult } from 'dugite'
import { getStatus } from './status'
import { RebaseContext } from '../../models/rebase'
import { RebaseContext, RebaseProgressOptions } from '../../models/rebase'
import { merge } from '../merge'
import { IRebaseProgress } from '../../models/progress'
/**
* Check the `.git/REBASE_HEAD` file exists in a repository to confirm
@ -81,6 +86,73 @@ export async function getRebaseContext(
return null
}
/** Regex for identifying when rebase applied each commit onto the base branch */
const rebaseApplyingRe = /^Applying: (.*)/
/**
* A parser to read and emit rebase progress from Git `stdout`
*/
class GitRebaseParser {
private currentCommitCount = 0
public constructor(
startCount: number,
private readonly totalCommitCount: number
) {
this.currentCommitCount = startCount
}
public parse(line: string): IRebaseProgress | null {
const match = rebaseApplyingRe.exec(line)
if (match === null || match.length !== 2) {
// Git will sometimes emit other output (for example, when it tries to
// resolve conflicts) and this does not match the expected output
return null
}
const commitSummary = match[1]
this.currentCommitCount++
const value = this.currentCommitCount / this.totalCommitCount
return {
kind: 'rebase',
title: `Rebasing commit ${this.currentCommitCount} of ${
this.totalCommitCount
} commits`,
value,
commitSummary,
}
}
}
function configureOptionsForRebase(
options: IGitExecutionOptions,
progress?: RebaseProgressOptions
) {
if (progress === undefined) {
return options
}
const { start, total, progressCallback } = progress
return merge(options, {
processCallback: (process: ChildProcess) => {
const parser = new GitRebaseParser(start, total)
// rebase emits progress messages on `stdout`, not `stderr`
byline(process.stdout).on('data', (line: string) => {
const progress = parser.parse(line)
if (progress != null) {
progressCallback(progress)
}
})
},
})
}
/**
* A stub function to use for initiating rebase in the app.
*
@ -91,19 +163,29 @@ export async function getRebaseContext(
* and it will probably have a different commit history.
*
* @param baseBranch the ref to start the rebase from
* @param featureBranch the ref to rebase onto `baseBranch`
* @param targetBranch the ref to rebase onto `baseBranch`
*/
export async function rebase(
repository: Repository,
baseBranch: string,
featureBranch: string
) {
return await git(
['rebase', baseBranch, featureBranch],
targetBranch: string,
progress?: RebaseProgressOptions
): Promise<RebaseResult> {
const options = configureOptionsForRebase(
{
expectedErrors: new Set([GitError.RebaseConflicts]),
},
progress
)
const result = await git(
['rebase', baseBranch, targetBranch],
repository.path,
'rebase',
{ expectedErrors: new Set([GitError.RebaseConflicts]) }
options
)
return parseRebaseResult(result)
}
/** Abandon the current rebase operation */
@ -111,28 +193,42 @@ export async function abortRebase(repository: Repository) {
await git(['rebase', '--abort'], repository.path, 'abortRebase')
}
export enum ContinueRebaseResult {
/** The app-specific results from attempting to rebase a repository */
export enum RebaseResult {
/**
* Git completed the rebase without reporting any errors, and the caller can
* signal success to the user.
*/
CompletedWithoutError = 'CompletedWithoutError',
/**
* The rebase encountered conflicts while attempting to rebase, and these
* need to be resolved by the user before the rebase can continue.
*/
ConflictsEncountered = 'ConflictsEncountered',
/**
* The rebase was not able to continue as tracked files were not staged in
* the index.
*/
OutstandingFilesNotStaged = 'OutstandingFilesNotStaged',
/**
* The rebase was not attempted because it could not check the status of the
* repository. The caller needs to confirm the repository is in a usable
* state.
*/
Aborted = 'Aborted',
}
const rebaseEncounteredConflictsRe = /Resolve all conflicts manually, mark them as resolved/
const filesNotMergedRe = /You must edit all merge conflicts and then\nmark them as resolved/
function parseRebaseResult(result: IGitResult): ContinueRebaseResult {
function parseRebaseResult(result: IGitResult): RebaseResult {
if (result.exitCode === 0) {
return ContinueRebaseResult.CompletedWithoutError
return RebaseResult.CompletedWithoutError
}
if (rebaseEncounteredConflictsRe.test(result.stdout)) {
return ContinueRebaseResult.ConflictsEncountered
if (result.gitError === GitError.RebaseConflicts) {
return RebaseResult.ConflictsEncountered
}
if (filesNotMergedRe.test(result.stdout)) {
return ContinueRebaseResult.OutstandingFilesNotStaged
if (result.gitError === GitError.UnresolvedConflicts) {
return RebaseResult.OutstandingFilesNotStaged
}
throw new Error(`Unhandled result found: '${JSON.stringify(result)}'`)
@ -149,8 +245,13 @@ function parseRebaseResult(result: IGitResult): ContinueRebaseResult {
export async function continueRebase(
repository: Repository,
files: ReadonlyArray<WorkingDirectoryFileChange>,
manualResolutions: ReadonlyMap<string, ManualConflictResolution> = new Map()
): Promise<ContinueRebaseResult> {
manualResolutions: ReadonlyMap<string, ManualConflictResolution> = new Map(),
progress?: RebaseProgressOptions
): Promise<RebaseResult> {
const trackedFiles = files.filter(f => {
return f.status.kind !== AppFileStatusKind.Untracked
})
// apply conflict resolutions
for (const [path, resolution] of manualResolutions) {
const file = files.find(f => f.path === path)
@ -163,7 +264,7 @@ export async function continueRebase(
}
}
const otherFiles = files.filter(f => !manualResolutions.has(f.path))
const otherFiles = trackedFiles.filter(f => !manualResolutions.has(f.path))
await stageFiles(repository, otherFiles)
@ -173,45 +274,46 @@ export async function continueRebase(
log.warn(
`[rebase] unable to get status after staging changes, skipping any other steps`
)
return ContinueRebaseResult.Aborted
return RebaseResult.Aborted
}
const trackedFiles = status.workingDirectory.files.filter(
const trackedFilesAfter = status.workingDirectory.files.filter(
f => f.status.kind !== AppFileStatusKind.Untracked
)
if (trackedFiles.length === 0) {
const options = configureOptionsForRebase(
{
expectedErrors: new Set([
GitError.RebaseConflicts,
GitError.UnresolvedConflicts,
]),
},
progress
)
if (trackedFilesAfter.length === 0) {
const rebaseHead = Path.join(repository.path, '.git', 'REBASE_HEAD')
const rebaseCurrentCommit = await FSE.readFile(rebaseHead)
const rebaseCurrentCommit = await FSE.readFile(rebaseHead, 'utf8')
log.warn(
`[rebase] no tracked changes to commit for ${rebaseCurrentCommit}, continuing rebase but skipping this commit`
`[rebase] no tracked changes to commit for ${rebaseCurrentCommit.trim()}, continuing rebase but skipping this commit`
)
const result = await git(
['rebase', '--skip'],
repository.path,
'continueRebaseSkipCurrentCommit',
{
successExitCodes: new Set([0, 1, 128]),
}
options
)
return parseRebaseResult(result)
}
// TODO: there are some cases we need to handle and surface here:
// - rebase continued and completed without error
// - rebase continued but encountered a different set of conflicts
// - rebase could not continue as there are outstanding conflicts
const result = await git(
['rebase', '--continue'],
repository.path,
'continueRebase',
{
successExitCodes: new Set([0, 1, 128]),
}
options
)
return parseRebaseResult(result)

84
app/src/lib/git/stash.ts Normal file
View file

@ -0,0 +1,84 @@
import { git } from '.'
import { Repository } from '../../models/repository'
export const DesktopStashEntryMarker = '!!GitHub_Desktop'
export interface IStashEntry {
/** The name of the branch at the time the entry was created. */
readonly branchName: string
/** The SHA of the commit object created as a result of stashing. */
readonly stashSha: string
}
/** RegEx for parsing out the stash SHA and message */
const stashEntryRe = /^([0-9a-f]{40})@(.+)$/
/**
* RegEx for determining if a stash entry is created by Desktop
*
* This is done by looking for a magic string with the following
* format: `!!GitHub_Desktop<branch@commit>`
*/
const stashEntryMessageRe = /^!!GitHub_Desktop<(.+)@([0-9|a-z|A-Z]{40})>$/
/**
* Get the list of stash entries created by Desktop in the current repository
*/
export async function getDesktopStashEntries(
repository: Repository
): Promise<ReadonlyArray<IStashEntry>> {
const prettyFormat = '%H@%gs'
const result = await git(
['log', '-g', 'refs/stash', `--pretty=${prettyFormat}`],
repository.path,
'getStashEntries'
)
if (result.stderr !== '') {
//don't really care what the error is right now, but will once dugite is updated
throw new Error(result.stderr)
}
const out = result.stdout
const lines = out.split('\n')
const stashEntries: Array<IStashEntry> = []
for (const line of lines) {
const match = stashEntryRe.exec(line)
if (match == null) {
continue
}
const message = match[2]
const branchName = extractBranchFromMessage(message)
// if branch name is null, the stash entry isn't using our magic string
if (branchName === null) {
continue
}
stashEntries.push({
branchName: branchName,
stashSha: match[1],
})
}
return stashEntries
}
function extractBranchFromMessage(message: string): string | null {
const [, desktopMessage] = message.split(':').map(s => s.trim())
const match = stashEntryMessageRe.exec(desktopMessage)
if (match === null) {
return null
}
const branchName = match[1]
return branchName.length > 0 ? branchName : null
}
export function createStashMessage(branchName: string, tipSha: string) {
return `${DesktopStashEntryMarker}<${branchName}@${tipSha}>`
}

View file

@ -112,6 +112,7 @@ export async function stageFiles(
const normal = []
const oldRenamed = []
const partial = []
const deletedFiles = []
for (const file of files) {
if (file.selection.getSelectionType() === DiffSelectionType.All) {
@ -119,6 +120,10 @@ export async function stageFiles(
if (file.status.kind === AppFileStatusKind.Renamed) {
oldRenamed.push(file.status.oldPath)
}
if (file.status.kind === AppFileStatusKind.Deleted) {
deletedFiles.push(file.path)
}
} else {
partial.push(file)
}
@ -151,6 +156,13 @@ export async function stageFiles(
// paths.
await updateIndex(repository, normal)
// This third step will only happen if we have files that have been marked
// for deletion. This covers us for files that were blown away in the last
// updateIndex call
if (deletedFiles.length > 0) {
await updateIndex(repository, deletedFiles, { forceRemove: true })
}
// Finally we run through all files that have partial selections.
// We don't care about renamed or not here since applyPatchToIndex
// has logic to support that scenario.

View file

@ -1,4 +1,4 @@
/* eslint-disable typescript/interface-name-prefix */
/* eslint-disable @typescript-eslint/interface-name-prefix */
/** Is the app running in dev mode? */
declare const __DEV__: boolean

View file

@ -35,7 +35,11 @@ export function getAvatarWithEnterpriseFallback(
email: string | null,
endpoint: string
): string {
if (endpoint === getDotComAPIEndpoint() || email === null) {
if (
endpoint === getDotComAPIEndpoint() ||
email === null ||
email.length === 0
) {
return avatar_url
}

View file

@ -104,6 +104,7 @@ const allMenuIds: ReadonlyArray<MenuIDs> = [
'update-branch',
'compare-to-branch',
'merge-branch',
'rebase-branch',
'view-repository-on-github',
'compare-on-github',
'open-in-shell',
@ -233,6 +234,7 @@ function getRepositoryMenuBuilder(state: IAppState): MenuStateBuilder {
onNonDefaultBranch && hasDefaultBranch && !onDetachedHead
)
menuStateBuilder.setEnabled('merge-branch', onBranch)
menuStateBuilder.setEnabled('rebase-branch', onBranch)
menuStateBuilder.setEnabled(
'compare-on-github',
isHostedOnGitHub && hasPublishedBranch
@ -287,6 +289,7 @@ function getRepositoryMenuBuilder(state: IAppState): MenuStateBuilder {
menuStateBuilder.disable('delete-branch')
menuStateBuilder.disable('update-branch')
menuStateBuilder.disable('merge-branch')
menuStateBuilder.disable('rebase-branch')
menuStateBuilder.disable('push')
menuStateBuilder.disable('pull')

View file

@ -41,7 +41,7 @@ export type URLActionType =
| IOpenRepositoryFromPathAction
| IUnknownAction
// eslint-disable-next-line typescript/interface-name-prefix
// eslint-disable-next-line @typescript-eslint/interface-name-prefix
interface ParsedUrlQueryWithUndefined {
// `undefined` is added here to ensure we handle the missing querystring key
// See https://github.com/Microsoft/TypeScript/issues/13778 for discussion about

View file

@ -186,9 +186,9 @@ export class GitProgressParser {
}
/**
* Parse the given line of output from Git, returns either an IGitProgress
* Parse the given line of output from Git, returns either an `IGitProgress`
* instance if the line could successfully be parsed as a Git progress
* event whose title was registered with this parser or an IGitOutput
* event whose title was registered with this parser or an `IGitOutput`
* instance if the line couldn't be parsed or if the title wasn't
* registered with the parser.
*/

View file

@ -1,5 +1,6 @@
import * as Fs from 'fs'
import * as Path from 'path'
import { encodePathAsUrl } from './path'
/**
* Type representing the contents of the gemoji json database
@ -28,7 +29,7 @@ interface IGemojiDefinition {
}
function getEmojiImageUrlFromRelativePath(relativePath: string): string {
return `file://${Path.join(__dirname, 'emoji', relativePath)}`
return encodePathAsUrl(__dirname, 'emoji', relativePath)
}
/**

View file

@ -163,6 +163,9 @@ export interface IDailyMeasures {
/** The number of times a user has pulled with `pull.rebase` unset or set to `false` */
readonly pullWithDefaultSettingCount: number
/** The number of times the user opens the "Rebase current branch" menu item */
readonly rebaseCurrentBranchMenuCount: number
}
export class StatsDatabase extends Dexie {

View file

@ -87,6 +87,7 @@ const DefaultDailyMeasures: IDailyMeasures = {
rebaseSuccessAfterConflictsCount: 0,
pullWithRebaseCount: 0,
pullWithDefaultSettingCount: 0,
rebaseCurrentBranchMenuCount: 0,
}
interface IOnboardingStats {
@ -616,6 +617,12 @@ export class StatsStore implements IStatsStore {
}))
}
public recordMenuInitiatedRebase(): Promise<void> {
return this.updateDailyMeasures(m => ({
rebaseCurrentBranchMenuCount: m.rebaseCurrentBranchMenuCount + 1,
}))
}
/** Record that the user checked out a PR branch */
public recordPRBranchCheckout(): Promise<void> {
return this.updateDailyMeasures(m => ({

View file

@ -73,7 +73,7 @@ import {
getAccountForEndpoint,
getDotComAPIEndpoint,
getEnterpriseAPIURL,
IAPIUser,
IAPIOrganization,
} from '../api'
import { shell } from '../app-shell'
import {
@ -137,6 +137,8 @@ import {
isGitRepository,
abortRebase,
continueRebase,
rebase,
PushOptions,
} from '../git'
import {
installGlobalLFSFilters,
@ -193,6 +195,7 @@ import {
import { BranchPruner } from './helpers/branch-pruner'
import { enableBranchPruning, enablePullWithRebase } from '../feature-flag'
import { Banner, BannerType } from '../../models/banner'
import { RebaseProgressOptions } from '../../models/rebase'
/**
* As fast-forwarding local branches is proportional to the number of local
@ -211,8 +214,10 @@ const commitSummaryWidthConfigKey: string = 'commit-summary-width'
const confirmRepoRemovalDefault: boolean = true
const confirmDiscardChangesDefault: boolean = true
const askForConfirmationOnForcePushDefault = true
const confirmRepoRemovalKey: string = 'confirmRepoRemoval'
const confirmDiscardChangesKey: string = 'confirmDiscardChanges'
const confirmForcePushKey: string = 'confirmForcePush'
const externalEditorKey: string = 'externalEditor'
@ -288,6 +293,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
private confirmRepoRemoval: boolean = confirmRepoRemovalDefault
private confirmDiscardChanges: boolean = confirmDiscardChangesDefault
private askForConfirmationOnForcePush = askForConfirmationOnForcePushDefault
private imageDiffType: ImageDiffType = imageDiffTypeDefault
private selectedExternalEditor?: ExternalEditor
@ -531,6 +537,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
currentBanner: this.currentBanner,
askForConfirmationOnRepositoryRemoval: this.confirmRepoRemoval,
askForConfirmationOnDiscardChanges: this.confirmDiscardChanges,
askForConfirmationOnForcePush: this.askForConfirmationOnForcePush,
selectedExternalEditor: this.selectedExternalEditor,
imageDiffType: this.imageDiffType,
selectedShell: this.selectedShell,
@ -1455,6 +1462,11 @@ export class AppStore extends TypedBaseStore<IAppState> {
confirmDiscardChangesDefault
)
this.askForConfirmationOnForcePush = getBoolean(
confirmForcePushKey,
askForConfirmationOnForcePushDefault
)
const externalEditorValue = await this.getSelectedExternalEditor()
if (externalEditorValue) {
this.selectedExternalEditor = externalEditorValue
@ -2585,15 +2597,19 @@ export class AppStore extends TypedBaseStore<IAppState> {
}
}
public async _push(repository: Repository): Promise<void> {
public async _push(
repository: Repository,
options?: PushOptions
): Promise<void> {
return this.withAuthenticatingUser(repository, (repository, account) => {
return this.performPush(repository, account)
return this.performPush(repository, account, options)
})
}
private async performPush(
repository: Repository,
account: IGitAccount | null
account: IGitAccount | null,
options?: PushOptions
): Promise<void> {
const state = this.repositoryStateCache.get(repository)
const { remote } = state
@ -2620,7 +2636,9 @@ export class AppStore extends TypedBaseStore<IAppState> {
if (tip.kind === TipState.Valid) {
const { branch } = tip
const pushTitle = `Pushing to ${remote.name}`
const remoteName = branch.remote || remote.name
const pushTitle = `Pushing to ${remoteName}`
// Emit an initial progress even before our push begins
// since we're doing some work to get remotes up front.
@ -2628,7 +2646,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
kind: 'push',
title: pushTitle,
value: 0,
remote: remote.name,
remote: remoteName,
branch: branch.name,
})
@ -2657,9 +2675,10 @@ export class AppStore extends TypedBaseStore<IAppState> {
await pushRepo(
repository,
account,
remote.name,
remoteName,
branch.name,
branch.upstreamWithoutRemote,
options,
progress => {
this.updatePushPullFetchProgress(repository, {
...progress,
@ -2978,7 +2997,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
description: string,
private_: boolean,
account: Account,
org: IAPIUser | null
org: IAPIOrganization | null
): Promise<Repository> {
const api = API.fromAccount(account)
const apiRepository = await api.createRepository(
@ -3353,7 +3372,20 @@ export class AppStore extends TypedBaseStore<IAppState> {
}
/** This shouldn't be called directly. See `Dispatcher`. */
public async _abortRebase(repository: Repository): Promise<void> {
public async _rebase(
repository: Repository,
baseBranch: string,
targetBranch: string,
progress?: RebaseProgressOptions
) {
const gitStore = this.gitStoreCache.get(repository)
return await gitStore.performFailableOperation(() =>
rebase(repository, baseBranch, targetBranch, progress)
)
}
/** This shouldn't be called directly. See `Dispatcher`. */
public async _abortRebase(repository: Repository) {
const gitStore = this.gitStoreCache.get(repository)
return await gitStore.performFailableOperation(() =>
abortRebase(repository)
@ -3363,16 +3395,12 @@ export class AppStore extends TypedBaseStore<IAppState> {
/** This shouldn't be called directly. See `Dispatcher`. */
public async _continueRebase(
repository: Repository,
workingDirectory: WorkingDirectoryStatus
workingDirectory: WorkingDirectoryStatus,
manualResolutions: ReadonlyMap<string, ManualConflictResolution>
) {
const gitStore = this.gitStoreCache.get(repository)
const trackedFiles = workingDirectory.files.filter(f => {
return f.status.kind !== AppFileStatusKind.Untracked
})
return await gitStore.performFailableOperation(() =>
continueRebase(repository, trackedFiles)
continueRebase(repository, workingDirectory.files, manualResolutions)
)
}
@ -3511,6 +3539,13 @@ export class AppStore extends TypedBaseStore<IAppState> {
return Promise.resolve()
}
public _setConfirmForcePushSetting(value: boolean): Promise<void> {
this.askForConfirmationOnForcePush = value
setBoolean(confirmForcePushKey, value)
this.emitUpdate()
return Promise.resolve()
}
public _setExternalEditor(selectedEditor: ExternalEditor): Promise<void> {
this.selectedExternalEditor = selectedEditor
localStorage.setItem(externalEditorKey, selectedEditor)

View file

@ -1,7 +1,12 @@
import { Repository } from '../../models/repository'
import { Account } from '../../models/account'
import { GitHubRepository } from '../../models/github-repository'
import { API, getAccountForEndpoint, getDotComAPIEndpoint } from '../api'
import {
API,
getAccountForEndpoint,
getDotComAPIEndpoint,
IAPIIdentity,
} from '../api'
import {
GitHubUserDatabase,
IGitHubUser,
@ -12,6 +17,17 @@ import { fatalError } from '../fatal-error'
import { compare } from '../compare'
import { BaseStore } from './base-store'
function isValidAuthor(
author: IAPIIdentity | {} | null
): author is IAPIIdentity {
return (
author !== null &&
typeof author === 'object' &&
'avatar_url' in author &&
'login' in author
)
}
/**
* The store for GitHub users. This is used to match commit authors to GitHub
* users and avatars.
@ -177,7 +193,7 @@ export class GitHubUserStore extends BaseStore {
public async _loadAndCacheUser(
accounts: ReadonlyArray<Account>,
repository: Repository,
sha: string | null,
sha: string,
email: string
) {
const endpoint = repository.gitHubRepository
@ -241,19 +257,22 @@ export class GitHubUserStore extends BaseStore {
private async findUserWithAPI(
account: Account,
repository: GitHubRepository,
sha: string | null,
sha: string,
email: string
): Promise<IGitHubUser | null> {
const api = API.fromAccount(account)
if (sha) {
const apiCommit = await api.fetchCommit(
repository.owner.login,
repository.name,
sha
)
if (apiCommit && apiCommit.author) {
if (apiCommit) {
const { author } = apiCommit
if (isValidAuthor(author)) {
const avatarURL = getAvatarWithEnterpriseFallback(
apiCommit.author.avatar_url,
author.avatar_url,
email,
account.endpoint
)
@ -261,9 +280,9 @@ export class GitHubUserStore extends BaseStore {
return {
email,
avatarURL,
login: apiCommit.author.login,
login: author.login,
endpoint: account.endpoint,
name: apiCommit.author.name || apiCommit.author.login,
name: author.login,
}
}
}
@ -280,7 +299,7 @@ export class GitHubUserStore extends BaseStore {
login: matchingUser.login,
avatarURL,
endpoint: account.endpoint,
name: matchingUser.name || matchingUser.login,
name: matchingUser.login,
}
}

View file

@ -1,7 +1,7 @@
const queue: (config: QueueConfig) => Queue = require('queue')
import { revSymmetricDifference } from '../../../lib/git'
// eslint-disable-next-line typescript/interface-name-prefix
// eslint-disable-next-line @typescript-eslint/interface-name-prefix
interface QueueConfig {
// Max number of jobs the queue should process concurrently, defaults to Infinity.
readonly concurrency: number
@ -10,7 +10,7 @@ interface QueueConfig {
readonly autostart: boolean
}
// eslint-disable-next-line typescript/interface-name-prefix
// eslint-disable-next-line @typescript-eslint/interface-name-prefix
interface Queue extends NodeJS.EventEmitter {
readonly length: number

View file

@ -9,6 +9,8 @@ import { IRemote } from '../../../models/remote'
*
* @param remotes A list of remotes for a given repository
*/
export function findDefaultRemote(remotes: ReadonlyArray<IRemote>) {
export function findDefaultRemote(
remotes: ReadonlyArray<IRemote>
): IRemote | null {
return remotes.find(x => x.name === 'origin') || remotes[0] || null
}

View file

@ -127,6 +127,7 @@ function getInitialRepositoryState(): IRepositoryState {
openPullRequests: new Array<PullRequest>(),
currentPullRequest: null,
isLoadingPullRequests: false,
rebasedBranches: new Map<string, string>(),
},
compareState: {
isDivergingBranchBannerVisible: false,

View file

@ -43,6 +43,14 @@ function initializeWinston(path: string): winston.LogMethod {
maxFiles: MaxLogFiles,
})
// The file logger handles errors when it can't write to an
// existing file but emits an error when attempting to create
// a file and failing (for example due to permissions or the
// disk being full). If logging fails that's not a big deal
// so we'll just suppress any error, besides, the console
// logger will likely still work.
fileLogger.on('error', () => {})
const consoleLogger = new winston.transports.Console({
level: process.env.NODE_ENV === 'development' ? 'debug' : 'error',
})

View file

@ -7,6 +7,7 @@ import { ensureDir } from 'fs-extra'
import { log } from '../log'
import { openDirectorySafe } from '../shell'
import { enableRebaseDialog } from '../../lib/feature-flag'
const defaultEditorLabel = __DARWIN__
? 'Open in External Editor'
@ -334,6 +335,15 @@ export function buildDefaultMenu({
accelerator: 'CmdOrCtrl+Shift+M',
click: emit('merge-branch'),
},
{
label: __DARWIN__
? 'Rebase Current Branch…'
: 'R&ebase current branch…',
id: 'rebase-branch',
accelerator: 'CmdOrCtrl+Shift+E',
click: emit('rebase-branch'),
visible: enableRebaseDialog(),
},
separator,
{
label: __DARWIN__ ? 'Compare on GitHub' : 'Compare on &GitHub',

View file

@ -16,6 +16,7 @@ export type MenuEvent =
| 'update-branch'
| 'compare-to-branch'
| 'merge-branch'
| 'rebase-branch'
| 'show-repository-settings'
| 'open-in-shell'
| 'compare-on-github'

View file

@ -4,6 +4,7 @@ export type MenuIDs =
| 'preferences'
| 'update-branch'
| 'merge-branch'
| 'rebase-branch'
| 'view-repository-on-github'
| 'compare-on-github'
| 'open-in-shell'

View file

@ -45,6 +45,8 @@ export enum PopupType {
PushNeedsPull,
LocalChangesOverwritten,
RebaseConflicts,
RebaseBranch,
ConfirmForcePush,
}
export type Popup =
@ -173,3 +175,13 @@ export type Popup =
baseBranch?: string
targetBranch: string
}
| {
type: PopupType.RebaseBranch
repository: Repository
branch?: Branch
}
| {
type: PopupType.ConfirmForcePush
repository: Repository
upstreamBranch: string
}

View file

@ -99,6 +99,13 @@ export interface IRevertProgress extends IProgress {
kind: 'revert'
}
/** An object describing the progress of a rebase operation */
export interface IRebaseProgress extends IProgress {
readonly kind: 'rebase'
/** The summary of the commit applied to the base branch */
readonly commitSummary: string
}
export type Progress =
| IGenericProgress
| ICheckoutProgress
@ -106,3 +113,4 @@ export type Progress =
| IPullProgress
| IPushProgress
| IRevertProgress
| IRebaseProgress

View file

@ -1,4 +1,4 @@
import { IAPIUser } from '../lib/api'
import { IAPIOrganization } from '../lib/api'
export type RepositoryPublicationSettings =
| IEnterprisePublicationSettings
@ -25,7 +25,7 @@ export interface IEnterprisePublicationSettings {
* The org to which this repository belongs. If null, the repository should be
* published as a personal repository.
*/
readonly org: IAPIUser | null
readonly org: IAPIOrganization | null
}
export interface IDotcomPublicationSettings {
@ -44,5 +44,5 @@ export interface IDotcomPublicationSettings {
* The org to which this repository belongs. If null, the repository should be
* published as a personal repository.
*/
readonly org: IAPIUser | null
readonly org: IAPIOrganization | null
}

View file

@ -1,5 +1,19 @@
import { IRebaseProgress } from './progress'
export type RebaseContext = {
readonly targetBranch: string
readonly baseBranchTip: string
readonly originalBranchTip: string
}
/**
* Options to pass in to rebase progress reporting
*/
export type RebaseProgressOptions = {
/** The number of commits already rebased as part of the operation */
start: number
/** The number of commits to be rebased as part of the operation */
total: number
/** The callback to fire when rebase progress is reported */
progressCallback: (progress: IRebaseProgress) => void
}

View file

@ -95,6 +95,8 @@ import { UsageStatsChange } from './usage-stats-change'
import { PushNeedsPullWarning } from './push-needs-pull'
import { LocalChangesOverwrittenWarning } from './local-changes-overwritten'
import { RebaseConflictsDialog } from './rebase'
import { RebaseBranchDialog } from './rebase/rebase-branch-dialog'
import { ConfirmForcePush } from './rebase/confirm-force-push'
const MinuteInMilliseconds = 1000 * 60
const HourInMilliseconds = MinuteInMilliseconds * 60
@ -323,6 +325,10 @@ export class App extends React.Component<IAppProps, IAppState> {
this.props.dispatcher.recordMenuInitiatedMerge()
return this.mergeBranch()
}
case 'rebase-branch': {
this.props.dispatcher.recordMenuInitiatedRebase()
return this.showRebaseDialog()
}
case 'show-repository-settings':
return this.showRepositorySettings()
case 'view-repository-on-github':
@ -927,6 +933,18 @@ export class App extends React.Component<IAppProps, IAppState> {
return repositories
}
private showRebaseDialog() {
const repository = this.getRepository()
if (!repository || repository instanceof CloningRepository) {
return
}
this.props.dispatcher.showPopup({
type: PopupType.RebaseBranch,
repository,
})
}
private showRepositorySettings() {
const repository = this.getRepository()
@ -1158,6 +1176,7 @@ export class App extends React.Component<IAppProps, IAppState> {
confirmDiscardChanges={
this.state.askForConfirmationOnDiscardChanges
}
confirmForcePush={this.state.askForConfirmationOnForcePush}
selectedExternalEditor={this.state.selectedExternalEditor}
optOutOfUsageTracking={this.props.appStore.getStatsOptOut()}
enterpriseAccount={this.getEnterpriseAccount()}
@ -1539,6 +1558,34 @@ export class App extends React.Component<IAppProps, IAppState> {
/>
)
}
case PopupType.RebaseBranch: {
const { repository, branch } = popup
const state = this.props.repositoryStateManager.get(repository)
const tip = state.branchesState.tip
// we should never get in this state since we disable the menu
// item in a detatched HEAD state, this check is so TSC is happy
if (tip.kind !== TipState.Valid) {
return null
}
const currentBranch = tip.branch
return (
<RebaseBranchDialog
key="merge-branch"
dispatcher={this.props.dispatcher}
repository={repository}
allBranches={state.branchesState.allBranches}
defaultBranch={state.branchesState.defaultBranch}
recentBranches={state.branchesState.recentBranches}
currentBranch={currentBranch}
initialBranch={branch}
onDismissed={this.onPopupDismissed}
/>
)
}
case PopupType.RebaseConflicts: {
const { selectedState } = this.state
@ -1573,6 +1620,19 @@ export class App extends React.Component<IAppProps, IAppState> {
/>
)
}
case PopupType.ConfirmForcePush: {
const { askForConfirmationOnForcePush } = this.state
return (
<ConfirmForcePush
dispatcher={this.props.dispatcher}
repository={popup.repository}
upstreamBranch={popup.upstreamBranch}
askForConfirmationOnForcePush={askForConfirmationOnForcePush}
onDismissed={this.onPopupDismissed}
/>
)
}
default:
return assertNever(popup, `Unknown popup type: ${popup}`)
}
@ -1835,7 +1895,7 @@ export class App extends React.Component<IAppProps, IAppState> {
return <RevertProgress progress={revertProgress} />
}
const remoteName = state.remote ? state.remote.name : null
let remoteName = state.remote ? state.remote.name : null
const progress = state.pushPullFetchProgress
const { conflictState } = state.changesState
@ -1843,8 +1903,18 @@ export class App extends React.Component<IAppProps, IAppState> {
const rebaseInProgress =
conflictState !== null && conflictState.kind === 'rebase'
const tipState = state.branchesState.tip.kind
const { pullWithRebase } = state.branchesState
const { pullWithRebase, tip, rebasedBranches } = state.branchesState
if (tip.kind === TipState.Valid && tip.branch.remote !== null) {
remoteName = tip.branch.remote
}
let branchWasRebased = false
if (tip.kind === TipState.Valid) {
const localBranchName = tip.branch.nameWithoutRemote
const { sha } = tip.branch.tip
const foundEntry = rebasedBranches.get(localBranchName)
branchWasRebased = foundEntry === sha
}
return (
<PushPullButton
@ -1855,9 +1925,10 @@ export class App extends React.Component<IAppProps, IAppState> {
lastFetched={state.lastFetched}
networkActionInProgress={state.isPushPullFetchInProgress}
progress={progress}
tipState={tipState}
tipState={tip.kind}
pullWithRebase={pullWithRebase}
rebaseInProgress={rebaseInProgress}
branchWasRebased={branchWasRebased}
/>
)
}

View file

@ -16,7 +16,7 @@ export function SuccessfulRebase({
<span>
{'Successfully rebased '}
<strong>{targetBranch}</strong>
{' on '}
{' onto '}
<strong>{baseBranch}</strong>
</span>
) : (

View file

@ -175,8 +175,16 @@ export class BranchesContainer extends React.Component<
}
private renderPullRequests() {
if (this.props.isLoadingPullRequests) {
return <PullRequestsLoading key="prs-loading" />
if (
this.props.isLoadingPullRequests &&
this.props.pullRequests.length === 0
) {
return (
<PullRequestsLoading
key="prs-loading"
renderPostFilter={this.renderPullRequestPostFilter}
/>
)
}
const pullRequests = this.props.pullRequests
@ -200,10 +208,30 @@ export class BranchesContainer extends React.Component<
onFilterTextChanged={this.onPullRequestFilterTextChanged}
onItemClick={this.onPullRequestClicked}
onDismiss={this.onDismiss}
renderPostFilter={this.renderPullRequestPostFilter}
/>
)
}
private onRefreshPullRequests = () => {
this.props.dispatcher.refreshPullRequests(this.props.repository)
}
private renderPullRequestPostFilter = () => {
return (
<Button
disabled={this.props.isLoadingPullRequests}
onClick={this.onRefreshPullRequests}
tooltip="Refresh the list of pull requests"
>
<Octicon
symbol={OcticonSymbol.sync}
className={this.props.isLoadingPullRequests ? 'spin' : undefined}
/>
</Button>
)
}
private onTabClicked = (tab: BranchesTab) => {
this.props.dispatcher.changeBranchesTab(tab)
}

View file

@ -50,6 +50,9 @@ interface IPullRequestListProps {
/** Called when the user opts to create a pull request */
readonly onCreatePullRequest: () => void
/** Called to render content after the filter. */
readonly renderPostFilter?: () => JSX.Element | null
/** Callback fired when user selects a new pull request */
readonly onSelectionChanged?: (
pullRequest: PullRequest | null,
@ -133,6 +136,7 @@ export class PullRequestList extends React.Component<
onSelectionChanged={this.onSelectionChanged}
onFilterKeyDown={this.props.onFilterKeyDown}
renderNoItems={this.renderNoItems}
renderPostFilter={this.props.renderPostFilter}
/>
)
}

View file

@ -24,24 +24,33 @@ const prLoadingItemProps: IPullRequestListItemProps = {
},
}
/** The placeholder for when pull requests are still loading. */
export class PullRequestsLoading extends React.Component<{}, {}> {
public render() {
const items: Array<IFilterListItem> = []
for (let i = 0; i < FacadeCount; i++) {
const items: Array<IFilterListItem> = []
for (let i = 0; i < FacadeCount; i++) {
items.push({
text: [''],
id: i.toString(),
})
}
}
const groups = [
const groups = [
{
identifier: '',
items,
items: items,
},
]
]
interface IPullRequestLoadingProps {
/** Called to render content after the filter. */
readonly renderPostFilter?: () => JSX.Element | null
}
/** The placeholder for when pull requests are still loading. */
export class PullRequestsLoading extends React.PureComponent<
IPullRequestLoadingProps,
{}
> {
public render() {
return (
<FilterList<IFilterListItem>
className="pull-request-list"
@ -51,6 +60,7 @@ export class PullRequestsLoading extends React.Component<{}, {}> {
renderItem={this.renderItem}
invalidationProps={groups}
disabled={true}
renderPostFilter={this.props.renderPostFilter}
/>
)
}

View file

@ -21,7 +21,6 @@ import { showContextualMenu } from '../main-process-proxy'
import { Octicon, OcticonSymbol } from '../octicons'
import { IAuthor } from '../../models/author'
import { IMenuItem } from '../../lib/menu-item'
import { shallowEquals } from '../../lib/equality'
import { ICommitContext } from '../../models/commit'
const addAuthorIcon = new OcticonSymbol(
@ -122,6 +121,29 @@ export class CommitMessage extends React.Component<
this.props.dispatcher.setCommitMessage(this.props.repository, this.state)
}
/**
* Special case for the summary/description being reset (empty) after a commit
* and the commit state changing thereafter, needing a sync with incoming props.
* We prefer the current UI state values if the user updated them manually.
*
* NOTE: although using the lifecycle method is generally an anti-pattern, we
* (and the React docs) believe it to be the right answer for this situation, see:
* https://reactjs.org/docs/react-component.html#unsafe_componentwillreceiveprops
*/
public componentWillReceiveProps(nextProps: ICommitMessageProps) {
const { commitMessage } = nextProps
if (!commitMessage || commitMessage === this.props.commitMessage) {
return
}
if (this.state.summary === '' && !this.state.description) {
this.setState({
summary: commitMessage.summary,
description: commitMessage.description,
})
}
}
public componentDidUpdate(prevProps: ICommitMessageProps) {
if (
this.props.autocompletionProviders !== prevProps.autocompletionProviders
@ -136,17 +158,6 @@ export class CommitMessage extends React.Component<
if (this.props.focusCommitMessage) {
this.focusSummary()
}
if (!shallowEquals(prevProps.commitMessage, this.props.commitMessage)) {
if (this.props.commitMessage) {
this.setState({
summary: this.props.commitMessage.summary,
description: this.props.commitMessage.description,
})
} else {
this.setState({ summary: '', description: null })
}
}
}
private clearCommitMessage() {

View file

@ -16,14 +16,13 @@ interface IContinueRebaseProps {
}
export class ContinueRebase extends React.Component<IContinueRebaseProps, {}> {
private onSubmit = () => {
this.continueRebase()
}
private onSubmit = async () => {
const { manualResolutions } = this.props.rebaseConflictState
private async continueRebase() {
await this.props.dispatcher.continueRebase(
this.props.repository,
this.props.workingDirectory
this.props.workingDirectory,
manualResolutions
)
}

View file

@ -31,7 +31,7 @@ interface IDialogProps {
* By omitting this consumers may use their own custom DialogHeader
* for when the default component doesn't cut it.
*/
readonly title?: string
readonly title?: string | JSX.Element
/**
* Whether or not the dialog should be dismissable. A dismissable dialog
@ -187,8 +187,12 @@ export class Dialog extends React.Component<IDialogProps, IDialogState> {
}
if (this.props.title) {
// createUniqueId handles static strings fine, so in the case of receiving
// a JSX element for the title we can just pass in a fixed value rather
// than trying to generate a string from an arbitrary element
const id = typeof this.props.title === 'string' ? this.props.title : '???'
this.setState({
titleId: createUniqueId(`Dialog_${this.props.id}_${this.props.title}`),
titleId: createUniqueId(`Dialog_${this.props.id}_${id}`),
})
}
}

View file

@ -60,6 +60,7 @@ export class DiscardChanges extends React.Component<
public render() {
const discardingAllChanges = this.props.discardingAllChanges
const isDiscardingChanges = this.state.isDiscardingChanges
return (
<Dialog
@ -70,6 +71,8 @@ export class DiscardChanges extends React.Component<
: toPlatformCase('Confirm Discard Changes')
}
onDismissed={this.props.onDismissed}
dismissable={isDiscardingChanges ? false : true}
loading={isDiscardingChanges}
type="warning"
>
<DialogContent>
@ -83,8 +86,10 @@ export class DiscardChanges extends React.Component<
<DialogFooter>
<ButtonGroup destructive={true}>
<Button type="submit">Cancel</Button>
<Button onClick={this.discard}>
<Button disabled={isDiscardingChanges} type="submit">
Cancel
</Button>
<Button onClick={this.discard} disabled={isDiscardingChanges}>
{discardingAllChanges
? toPlatformCase('Discard All Changes')
: toPlatformCase('Discard Changes')}

View file

@ -2,7 +2,7 @@ import { remote } from 'electron'
import { Disposable } from 'event-kit'
import * as Path from 'path'
import { IAPIUser } from '../../lib/api'
import { IAPIOrganization } from '../../lib/api'
import { shell } from '../../lib/app-shell'
import {
CompareAction,
@ -19,7 +19,7 @@ import {
setGenericPassword,
setGenericUsername,
} from '../../lib/generic-git-auth'
import { isGitRepository, ContinueRebaseResult } from '../../lib/git'
import { isGitRepository, RebaseResult, PushOptions } from '../../lib/git'
import { isGitOnPath } from '../../lib/is-git-on-path'
import {
rejectOAuthRequest,
@ -64,7 +64,8 @@ import {
WorkingDirectoryFileChange,
WorkingDirectoryStatus,
} from '../../models/status'
import { TipState } from '../../models/tip'
import { TipState, IValidBranch } from '../../models/tip'
import { RebaseProgressOptions } from '../../models/rebase'
import { Banner, BannerType } from '../../models/banner'
import { ApplicationTheme } from '../lib/application-theme'
@ -321,8 +322,12 @@ export class Dispatcher {
}
/** Push the current branch. */
public push(repository: Repository): Promise<void> {
return this.appStore._push(repository)
public push(repository: Repository, options?: PushOptions): Promise<void> {
if (options !== undefined && options.forceWithLease) {
this.dropCurrentBranchFromForcePushList(repository)
}
return this.appStore._push(repository, options)
}
/** Pull the current branch. */
@ -350,7 +355,7 @@ export class Dispatcher {
description: string,
private_: boolean,
account: Account,
org: IAPIUser | null
org: IAPIOrganization | null
): Promise<Repository> {
return this.appStore._publishRepository(
repository,
@ -623,6 +628,100 @@ export class Dispatcher {
return this.appStore._mergeBranch(repository, branch, mergeStatus)
}
/**
* Update the per-repository list of branches that can be force-pushed
* after a rebase is completed.
*/
private addRebasedBranchToForcePushList = (
repository: Repository,
tipWithBranch: IValidBranch,
beforeRebaseSha: string
) => {
// if the commit id of the branch is unchanged, it can be excluded from
// this list
if (tipWithBranch.branch.tip.sha === beforeRebaseSha) {
return
}
const currentState = this.repositoryStateManager.get(repository)
const { rebasedBranches } = currentState.branchesState
const updatedMap = new Map<string, string>(rebasedBranches)
updatedMap.set(
tipWithBranch.branch.nameWithoutRemote,
tipWithBranch.branch.tip.sha
)
this.repositoryStateManager.updateBranchesState(repository, () => ({
rebasedBranches: updatedMap,
}))
}
private dropCurrentBranchFromForcePushList = (repository: Repository) => {
const currentState = this.repositoryStateManager.get(repository)
const { rebasedBranches, tip } = currentState.branchesState
if (tip.kind !== TipState.Valid) {
return
}
const updatedMap = new Map<string, string>(rebasedBranches)
updatedMap.delete(tip.branch.nameWithoutRemote)
this.repositoryStateManager.updateBranchesState(repository, () => ({
rebasedBranches: updatedMap,
}))
}
/** Starts a rebase for the given base and target branch */
public async rebase(
repository: Repository,
baseBranch: string,
targetBranch: string,
progress?: RebaseProgressOptions
) {
const stateBefore = this.repositoryStateManager.get(repository)
const beforeSha = getTipSha(stateBefore.branchesState.tip)
log.info(`[rebase] starting rebase for ${beforeSha}`)
// TODO: this can happen very quickly for a trivial rebase or an OS with
// fast I/O - are we able to artificially slow this down so it completes at
// least after X ms?
const result = await this.appStore._rebase(
repository,
baseBranch,
targetBranch,
progress
)
await this.appStore._loadStatus(repository)
const stateAfter = this.repositoryStateManager.get(repository)
const { tip } = stateAfter.branchesState
const afterSha = getTipSha(tip)
log.info(
`[rebase] completed rebase - got ${result} and on tip ${afterSha} - kind ${
tip.kind
}`
)
if (result === RebaseResult.CompletedWithoutError) {
if (tip.kind === TipState.Valid) {
this.addRebasedBranchToForcePushList(repository, tip, beforeSha)
}
this.setBanner({
type: BannerType.SuccessfulRebase,
targetBranch: targetBranch,
baseBranch: baseBranch,
})
}
}
/** aborts the current rebase and refreshes the repository's status */
public async abortRebase(repository: Repository) {
await this.appStore._abortRebase(repository)
@ -631,7 +730,8 @@ export class Dispatcher {
public async continueRebase(
repository: Repository,
workingDirectory: WorkingDirectoryStatus
workingDirectory: WorkingDirectoryStatus,
manualResolutions: ReadonlyMap<string, ManualConflictResolution>
) {
const stateBefore = this.repositoryStateManager.get(repository)
@ -641,7 +741,8 @@ export class Dispatcher {
const result = await this.appStore._continueRebase(
repository,
workingDirectory
workingDirectory,
manualResolutions
)
await this.appStore._loadStatus(repository)
@ -657,18 +758,24 @@ export class Dispatcher {
const { conflictState } = stateBefore.changesState
if (
result === ContinueRebaseResult.CompletedWithoutError &&
conflictState !== null &&
isRebaseConflictState(conflictState)
) {
if (result === RebaseResult.CompletedWithoutError) {
this.closePopup()
if (conflictState !== null && isRebaseConflictState(conflictState)) {
this.setBanner({
type: BannerType.SuccessfulRebase,
targetBranch: conflictState.targetBranch,
})
}
return result
if (tip.kind === TipState.Valid) {
this.addRebasedBranchToForcePushList(
repository,
tip,
conflictState.originalBranchTip
)
}
}
}
}
/** aborts an in-flight merge and refreshes the repository's status */
@ -1370,6 +1477,47 @@ export class Dispatcher {
)
}
public async confirmOrForcePush(repository: Repository) {
const { askForConfirmationOnForcePush } = this.appStore.getState()
const { branchesState } = this.repositoryStateManager.get(repository)
const { tip } = branchesState
if (tip.kind !== TipState.Valid) {
log.warn(`Could not find a branch to perform force push`)
return
}
const { upstream } = tip.branch
if (upstream === null) {
log.warn(`Could not find an upstream branch which will be pushed`)
return
}
if (askForConfirmationOnForcePush) {
this.showPopup({
type: PopupType.ConfirmForcePush,
repository,
upstreamBranch: upstream,
})
} else {
await this.performForcePush(repository)
}
}
public async performForcePush(repository: Repository) {
await this.push(repository, {
forceWithLease: true,
})
await this.loadStatus(repository)
}
public setConfirmForcePushSetting(value: boolean) {
return this.appStore._setConfirmForcePushSetting(value)
}
/**
* Updates the application state to indicate a conflict is in-progress
* as a result of a pull and increments the relevant metric.
@ -1393,6 +1541,13 @@ export class Dispatcher {
return this.statsStore.recordMenuInitiatedMerge()
}
/**
* Increments the `rebaseIntoCurrentBranchMenuCount` metric
*/
public recordMenuInitiatedRebase() {
return this.statsStore.recordMenuInitiatedRebase()
}
/**
* Increments the `updateFromDefaultBranchMenuCount` metric
*/
@ -1547,4 +1702,11 @@ export class Dispatcher {
public recordRebaseConflictsDialogReopened() {
this.statsStore.recordRebaseConflictsDialogReopened()
}
/**
* Refresh the list of open pull requests for the given repository.
*/
public refreshPullRequests(repository: Repository): Promise<void> {
return this.appStore._refreshPullRequests(repository)
}
}

View file

@ -205,7 +205,7 @@ function orderByPosition(x: ActualTextMarker, y: ActualTextMarker) {
// The types for CodeMirror.TextMarker is all wrong, this is what it
// actually looks like
// eslint-disable-next-line typescript/interface-name-prefix
// eslint-disable-next-line @typescript-eslint/interface-name-prefix
interface ActualTextMarker extends CodeMirror.TextMarkerOptions {
/** Remove the mark. */
clear(): void

View file

@ -0,0 +1,2 @@
export * from './render-functions'
export * from './unmerged-file'

View file

@ -0,0 +1,34 @@
import * as React from 'react'
import { Octicon, OcticonSymbol } from '../../octicons'
import { LinkButton } from '../link-button'
export function renderUnmergedFilesSummary(conflictedFilesCount: number) {
// localization, it burns :vampire:
const message =
conflictedFilesCount === 1
? `1 conflicted file`
: `${conflictedFilesCount} conflicted files`
return <h3 className="summary">{message}</h3>
}
export function renderAllResolved() {
return (
<div className="all-conflicts-resolved">
<div className="green-circle">
<Octicon symbol={OcticonSymbol.check} />
</div>
<div className="message">All conflicts resolved</div>
</div>
)
}
export function renderShellLink(openThisRepositoryInShell: () => void) {
return (
<div>
<LinkButton onClick={openThisRepositoryInShell}>
Open in command line,
</LinkButton>{' '}
your tool of choice, or close to resolve manually.
</div>
)
}

View file

@ -5,31 +5,31 @@ import {
ConflictedFileStatus,
ConflictsWithMarkers,
ManualConflict,
} from '../../models/status'
} from '../../../models/status'
import { join } from 'path'
import { Repository } from '../../models/repository'
import { Dispatcher } from '../dispatcher'
import { showContextualMenu } from '../main-process-proxy'
import { Octicon, OcticonSymbol } from '../octicons'
import { PathText } from '../lib/path-text'
import { Repository } from '../../../models/repository'
import { Dispatcher } from '../../dispatcher'
import { showContextualMenu } from '../../main-process-proxy'
import { Octicon, OcticonSymbol } from '../../octicons'
import { PathText } from '../path-text'
import {
ManualConflictResolutionKind,
ManualConflictResolution,
} from '../../models/manual-conflict-resolution'
} from '../../../models/manual-conflict-resolution'
import {
OpenWithDefaultProgramLabel,
RevealInFileManagerLabel,
} from '../lib/context-menu'
import { openFile } from '../lib/open-file'
} from '../context-menu'
import { openFile } from '../open-file'
import { shell } from 'electron'
import { Button } from '../lib/button'
import { IMenuItem } from '../../lib/menu-item'
import { LinkButton } from '../lib/link-button'
import { Button } from '../button'
import { IMenuItem } from '../../../lib/menu-item'
import { LinkButton } from '../link-button'
import {
hasUnresolvedConflicts,
getUnmergedStatusEntryDescription,
getLabelForManualResolutionOption,
} from '../../lib/status'
} from '../../../lib/status'
/**
* Renders an unmerged file status and associated buttons for the merge conflicts modal
@ -46,9 +46,25 @@ export const renderUnmergedFile: React.SFC<{
* (optional. only applies to manual merge conflicts)
*/
readonly manualResolution?: ManualConflictResolution
/** branch we were on before starting merge */
readonly ourBranch: string
/* `undefined` when we didn't know the branch at the beginning of the merge conflict resolution flow */
/**
* Current branch associated with the conflicted state for this file:
*
* - for a merge, this is the tip of the repository
* - for a rebase, this is the base branch that commits are being applied on top
*
* If the rebase is started outside Desktop, the details about this branch may
* not be known - the rendered component will handle this fine.
*/
readonly ourBranch?: string
/**
* The other branch associated with the conflicted state for this file:
*
* - for a merge, this is be the branch being merged into the tip of the repository
* - for a rebase, this is the target branch that is having it's history rewritten
*
* If the merge is started outside Desktop, the details about this branch may
* not be known - the rendered component will handle this fine.
*/
readonly theirBranch?: string
/** name of the resolved external editor */
readonly resolvedExternalEditor: string | null
@ -128,7 +144,7 @@ const renderManualConflictedFile: React.SFC<{
readonly path: string
readonly status: ManualConflict
readonly repository: Repository
readonly ourBranch: string
readonly ourBranch?: string
readonly theirBranch?: string
readonly dispatcher: Dispatcher
}> = props => {
@ -231,7 +247,7 @@ const makeManualConflictDropdownClickHandler = (
status: ManualConflict,
repository: Repository,
dispatcher: Dispatcher,
ourBranch: string,
ourBranch?: string,
theirBranch?: string
) => {
return () => {
@ -349,7 +365,7 @@ const renderResolvedFileStatusSummary: React.SFC<{
/** returns the name of the branch that corresponds to the chosen manual resolution */
function getBranchForResolution(
manualResolution: ManualConflictResolution | undefined,
ourBranch: string,
ourBranch?: string,
theirBranch?: string
): string | undefined {
if (manualResolution === ManualConflictResolutionKind.ours) {

View file

@ -10,9 +10,7 @@ import {
WorkingDirectoryStatus,
WorkingDirectoryFileChange,
} from '../../models/status'
import { Octicon, OcticonSymbol } from '../octicons'
import { DialogHeader } from '../dialog/header'
import { LinkButton } from '../lib/link-button'
import {
isConflictedFile,
getResolvedFiles,
@ -20,7 +18,12 @@ import {
getUnmergedFiles,
} from '../../lib/status'
import { DefaultCommitMessage } from '../../models/commit-message'
import { renderUnmergedFile } from './unmerged-file'
import {
renderUnmergedFile,
renderUnmergedFilesSummary,
renderShellLink,
renderAllResolved,
} from '../lib/conflicts'
import { ManualConflictResolution } from '../../models/manual-conflict-resolution'
import { BannerType } from '../../models/banner'
@ -145,17 +148,6 @@ export class MergeConflictsDialog extends React.Component<
private openThisRepositoryInShell = () =>
this.props.openRepositoryInShell(this.props.repository)
private renderShellLink(openThisRepositoryInShell: () => void): JSX.Element {
return (
<div>
<LinkButton onClick={openThisRepositoryInShell}>
Open in command line,
</LinkButton>{' '}
your tool of choice, or close to resolve manually.
</div>
)
}
private renderUnmergedFiles(
files: ReadonlyArray<WorkingDirectoryFileChange>
) {
@ -180,39 +172,19 @@ export class MergeConflictsDialog extends React.Component<
)
}
private renderUnmergedFilesSummary(conflictedFilesCount: number) {
// localization, it burns :vampire:
const message =
conflictedFilesCount === 1
? `1 conflicted file`
: `${conflictedFilesCount} conflicted files`
return <h3 className="summary">{message}</h3>
}
private renderAllResolved() {
return (
<div className="all-conflicts-resolved">
<div className="green-circle">
<Octicon symbol={OcticonSymbol.check} />
</div>
<div className="message">All conflicts resolved</div>
</div>
)
}
private renderContent(
unmergedFiles: ReadonlyArray<WorkingDirectoryFileChange>,
conflictedFilesCount: number
): JSX.Element {
if (unmergedFiles.length === 0) {
return this.renderAllResolved()
return renderAllResolved()
}
return (
<>
{this.renderUnmergedFilesSummary(conflictedFilesCount)}
{renderUnmergedFilesSummary(conflictedFilesCount)}
{this.renderUnmergedFiles(unmergedFiles)}
{this.renderShellLink(this.openThisRepositoryInShell)}
{renderShellLink(this.openThisRepositoryInShell)}
</>
)
}

View file

@ -15,6 +15,7 @@ interface IAdvancedPreferencesProps {
readonly optOutOfUsageTracking: boolean
readonly confirmRepositoryRemoval: boolean
readonly confirmDiscardChanges: boolean
readonly confirmForcePush: boolean
readonly availableEditors: ReadonlyArray<ExternalEditor>
readonly selectedExternalEditor?: ExternalEditor
readonly availableShells: ReadonlyArray<Shell>
@ -22,6 +23,7 @@ interface IAdvancedPreferencesProps {
readonly onOptOutofReportingchanged: (checked: boolean) => void
readonly onConfirmDiscardChangesChanged: (checked: boolean) => void
readonly onConfirmRepositoryRemovalChanged: (checked: boolean) => void
readonly onConfirmForcePushChanged: (checked: boolean) => void
readonly onSelectedEditorChanged: (editor: ExternalEditor) => void
readonly onSelectedShellChanged: (shell: Shell) => void
@ -36,6 +38,7 @@ interface IAdvancedPreferencesState {
readonly selectedShell: Shell
readonly confirmRepositoryRemoval: boolean
readonly confirmDiscardChanges: boolean
readonly confirmForcePush: boolean
}
export class Advanced extends React.Component<
@ -49,6 +52,7 @@ export class Advanced extends React.Component<
optOutOfUsageTracking: this.props.optOutOfUsageTracking,
confirmRepositoryRemoval: this.props.confirmRepositoryRemoval,
confirmDiscardChanges: this.props.confirmDiscardChanges,
confirmForcePush: this.props.confirmForcePush,
selectedExternalEditor: this.props.selectedExternalEditor,
selectedShell: this.props.selectedShell,
}
@ -101,6 +105,15 @@ export class Advanced extends React.Component<
this.props.onConfirmDiscardChangesChanged(value)
}
private onConfirmForcePushChanged = (
event: React.FormEvent<HTMLInputElement>
) => {
const value = event.currentTarget.checked
this.setState({ confirmForcePush: value })
this.props.onConfirmForcePushChanged(value)
}
private onConfirmRepositoryRemovalChanged = (
event: React.FormEvent<HTMLInputElement>
) => {
@ -260,6 +273,15 @@ export class Advanced extends React.Component<
onChange={this.onConfirmDiscardChangesChanged}
/>
</Row>
<Row>
<Checkbox
label="Show confirmation dialog before force pushing"
value={
this.state.confirmForcePush ? CheckboxValue.On : CheckboxValue.Off
}
onChange={this.onConfirmForcePushChanged}
/>
</Row>
</DialogContent>
)
}

View file

@ -33,6 +33,7 @@ interface IPreferencesProps {
readonly initialSelectedTab?: PreferencesTab
readonly confirmRepositoryRemoval: boolean
readonly confirmDiscardChanges: boolean
readonly confirmForcePush: boolean
readonly selectedExternalEditor?: ExternalEditor
readonly selectedShell: Shell
readonly selectedTheme: ApplicationTheme
@ -47,6 +48,7 @@ interface IPreferencesState {
readonly optOutOfUsageTracking: boolean
readonly confirmRepositoryRemoval: boolean
readonly confirmDiscardChanges: boolean
readonly confirmForcePush: boolean
readonly automaticallySwitchTheme: boolean
readonly availableEditors: ReadonlyArray<ExternalEditor>
readonly selectedExternalEditor?: ExternalEditor
@ -72,6 +74,7 @@ export class Preferences extends React.Component<
optOutOfUsageTracking: false,
confirmRepositoryRemoval: false,
confirmDiscardChanges: false,
confirmForcePush: false,
automaticallySwitchTheme: false,
selectedExternalEditor: this.props.selectedExternalEditor,
availableShells: [],
@ -119,6 +122,7 @@ export class Preferences extends React.Component<
optOutOfUsageTracking: this.props.optOutOfUsageTracking,
confirmRepositoryRemoval: this.props.confirmRepositoryRemoval,
confirmDiscardChanges: this.props.confirmDiscardChanges,
confirmForcePush: this.props.confirmForcePush,
availableShells,
availableEditors,
mergeTool,
@ -227,6 +231,7 @@ export class Preferences extends React.Component<
optOutOfUsageTracking={this.state.optOutOfUsageTracking}
confirmRepositoryRemoval={this.state.confirmRepositoryRemoval}
confirmDiscardChanges={this.state.confirmDiscardChanges}
confirmForcePush={this.state.confirmForcePush}
availableEditors={this.state.availableEditors}
selectedExternalEditor={this.state.selectedExternalEditor}
onOptOutofReportingchanged={this.onOptOutofReportingChanged}
@ -234,6 +239,7 @@ export class Preferences extends React.Component<
this.onConfirmRepositoryRemovalChanged
}
onConfirmDiscardChangesChanged={this.onConfirmDiscardChangesChanged}
onConfirmForcePushChanged={this.onConfirmForcePushChanged}
onSelectedEditorChanged={this.onSelectedEditorChanged}
availableShells={this.state.availableShells}
selectedShell={this.state.selectedShell}
@ -261,6 +267,10 @@ export class Preferences extends React.Component<
this.setState({ confirmDiscardChanges: value })
}
private onConfirmForcePushChanged = (value: boolean) => {
this.setState({ confirmForcePush: value })
}
private onCommitterNameChanged = (committerName: string) => {
const disallowedCharactersMessage = this.disallowedCharacterErrorMessage(
committerName,
@ -336,6 +346,10 @@ export class Preferences extends React.Component<
this.state.confirmRepositoryRemoval
)
await this.props.dispatcher.setConfirmForcePushSetting(
this.state.confirmForcePush
)
if (this.state.selectedExternalEditor) {
await this.props.dispatcher.setExternalEditor(
this.state.selectedExternalEditor

View file

@ -1,6 +1,6 @@
import * as React from 'react'
import { Account } from '../../models/account'
import { API, IAPIUser } from '../../lib/api'
import { API, IAPIOrganization } from '../../lib/api'
import { TextBox } from '../lib/text-box'
import { Select } from '../lib/select'
import { DialogContent } from '../dialog'
@ -23,7 +23,7 @@ interface IPublishRepositoryProps {
}
interface IPublishRepositoryState {
readonly orgs: ReadonlyArray<IAPIUser>
readonly orgs: ReadonlyArray<IAPIOrganization>
}
/** The Publish Repository component. */
@ -55,7 +55,8 @@ export class PublishRepository extends React.Component<
private async fetchOrgs(account: Account) {
const api = API.fromAccount(account)
const orgs = (await api.fetchOrgs()) as Array<IAPIUser>
const apiOrgs = await api.fetchOrgs()
const orgs = [...apiOrgs]
orgs.sort((a, b) => caseInsensitiveCompare(a.login, b.login))
this.setState({ orgs })
}

View file

@ -0,0 +1,89 @@
import * as React from 'react'
import { Dispatcher } from '../dispatcher'
import { ButtonGroup } from '../lib/button-group'
import { DialogFooter, DialogContent, Dialog } from '../dialog'
import { Button } from '../lib/button'
import { Repository } from '../../models/repository'
import { Checkbox, CheckboxValue } from '../lib/checkbox'
interface IConfirmForcePushProps {
readonly dispatcher: Dispatcher
readonly repository: Repository
readonly upstreamBranch: string
readonly askForConfirmationOnForcePush: boolean
readonly onDismissed: () => void
}
interface IConfirmForcePushState {
readonly isLoading: boolean
readonly askForConfirmationOnForcePush: boolean
}
export class ConfirmForcePush extends React.Component<
IConfirmForcePushProps,
IConfirmForcePushState
> {
public constructor(props: IConfirmForcePushProps) {
super(props)
this.state = {
isLoading: false,
askForConfirmationOnForcePush: props.askForConfirmationOnForcePush,
}
}
public render() {
return (
<Dialog
title="Are you sure you want to force push?"
dismissable={!this.state.isLoading}
onDismissed={this.props.onDismissed}
onSubmit={this.onForcePush}
type="warning"
>
<DialogContent>
<p>
A force push will rewrite history on{' '}
<strong>{this.props.upstreamBranch}</strong>. Any collaborators
working on this branch will need to reset their own local branch to
match the history of the remote.
</p>
<div>
<Checkbox
label="Do not show this message again"
value={
this.state.askForConfirmationOnForcePush
? CheckboxValue.Off
: CheckboxValue.On
}
onChange={this.onAskForConfirmationOnForcePushChanged}
/>
</div>
</DialogContent>
<DialogFooter>
<ButtonGroup>
<Button type="submit">I'm sure</Button>
<Button onClick={this.props.onDismissed}>Cancel</Button>
</ButtonGroup>
</DialogFooter>
</Dialog>
)
}
private onAskForConfirmationOnForcePushChanged = (
event: React.FormEvent<HTMLInputElement>
) => {
const value = !event.currentTarget.checked
this.setState({ askForConfirmationOnForcePush: value })
}
private onForcePush = async () => {
this.props.dispatcher.setConfirmForcePushSetting(
this.state.askForConfirmationOnForcePush
)
this.props.onDismissed()
await this.props.dispatcher.performForcePush(this.props.repository)
}
}

View file

@ -1,205 +0,0 @@
import { join } from 'path'
import * as React from 'react'
import { openFile } from '../lib/open-file'
import { showContextualMenu } from '../main-process-proxy'
import { hasUnresolvedConflicts } from '../../lib/status'
import { IMenuItem } from '../../lib/menu-item'
import { shell } from '../../lib/app-shell'
import { Repository } from '../../models/repository'
import {
AppFileStatusKind,
ConflictedFileStatus,
isConflictWithMarkers,
WorkingDirectoryFileChange,
} from '../../models/status'
import { Dispatcher } from '../dispatcher'
import { Button } from '../lib/button'
import { PathText } from '../lib/path-text'
import { Octicon, OcticonSymbol } from '../octicons'
import {
OpenWithDefaultProgramLabel,
RevealInFileManagerLabel,
} from '../lib/context-menu'
/**
* Calculates the number of merge conclicts in a file from the number of markers
* divides by three and rounds up since each conflict is indicated by three separate markers
* (`<<<<<`, `>>>>>`, and `=====`)
* @param conflictMarkers number of conflict markers in a file
*/
function calculateConflicts(conflictMarkers: number) {
return Math.ceil(conflictMarkers / 3)
}
function editorButtonString(editorName: string | null): string {
const defaultEditorString = 'editor'
return `Open in ${editorName || defaultEditorString}`
}
function editorButtonTooltip(editorName: string | null): string | undefined {
if (editorName !== null) {
// no need to render a tooltip if we have a known editor
return
}
if (__DARWIN__) {
return `No editor configured in Preferences > Advanced`
} else {
return `No editor configured in Options > Advanced`
}
}
interface IConflictedFilesListProps {
readonly dispatcher: Dispatcher
readonly repository: Repository
readonly files: ReadonlyArray<WorkingDirectoryFileChange>
readonly openFileInExternalEditor: (path: string) => void
readonly resolvedExternalEditor: string | null
}
export class ConflictedFilesList extends React.Component<
IConflictedFilesListProps,
{}
> {
private makeDropdownClickHandler = (
relativeFilePath: string,
repositoryFilePath: string,
dispatcher: Dispatcher
) => {
return () => {
const absoluteFilePath = join(repositoryFilePath, relativeFilePath)
const items: IMenuItem[] = [
{
label: OpenWithDefaultProgramLabel,
action: () => openFile(absoluteFilePath, dispatcher),
},
{
label: RevealInFileManagerLabel,
action: () => shell.showItemInFolder(absoluteFilePath),
},
]
showContextualMenu(items)
}
}
private renderResolvedFile(path: string): JSX.Element {
return (
<li key={path} className="unmerged-file-status-resolved">
<Octicon symbol={OcticonSymbol.fileCode} className="file-octicon" />
<div className="column-left">
<PathText path={path} availableWidth={200} />
<div className="file-conflicts-status">No conflicts remaining</div>
</div>
<div className="green-circle">
<Octicon symbol={OcticonSymbol.check} />
</div>
</li>
)
}
private renderConflictedFile(
path: string,
status: ConflictedFileStatus,
onOpenEditorClick: () => void
): JSX.Element | null {
let content = null
if (isConflictWithMarkers(status)) {
const humanReadableConflicts = calculateConflicts(
status.conflictMarkerCount
)
const message =
humanReadableConflicts === 1
? `1 conflict`
: `${humanReadableConflicts} conflicts`
const disabled = this.props.resolvedExternalEditor === null
const tooltip = editorButtonTooltip(this.props.resolvedExternalEditor)
const onDropdownClick = this.makeDropdownClickHandler(
path,
this.props.repository.path,
this.props.dispatcher
)
content = (
<>
<div className="column-left">
<PathText path={path} availableWidth={200} />
<div className="file-conflicts-status">{message}</div>
</div>
<div className="action-buttons">
<Button
onClick={onOpenEditorClick}
disabled={disabled}
tooltip={tooltip}
className="small-button button-group-item"
>
{editorButtonString(this.props.resolvedExternalEditor)}
</Button>
<Button
onClick={onDropdownClick}
className="small-button button-group-item arrow-menu"
>
<Octicon symbol={OcticonSymbol.triangleDown} />
</Button>
</div>
</>
)
} else {
content = (
<div>
<PathText path={path} availableWidth={400} />
<div className="command-line-hint">
Use command line to resolve this file
</div>
</div>
)
}
return content !== null ? (
<li key={path} className="unmerged-file-status-conflicts">
<Octicon symbol={OcticonSymbol.fileCode} className="file-octicon" />
{content}
</li>
) : null
}
private renderRow = (file: WorkingDirectoryFileChange) => {
const { status } = file
switch (status.kind) {
case AppFileStatusKind.Conflicted:
if (!hasUnresolvedConflicts(status)) {
return this.renderResolvedFile(file.path)
}
return this.renderConflictedFile(file.path, status, () =>
this.props.openFileInExternalEditor(
join(this.props.repository.path, file.path)
)
)
default:
return null
}
}
public render() {
if (this.props.files.length === 0) {
return (
<div className="all-conflicts-resolved">
<div className="green-circle">
<Octicon symbol={OcticonSymbol.check} />
</div>
<div className="message">All conflicts resolved</div>
</div>
)
}
return (
<ul className="unmerged-file-statuses">
{this.props.files.map(f => this.renderRow(f))}
</ul>
)
}
}

View file

@ -0,0 +1,198 @@
import * as React from 'react'
import { Dispatcher } from '../dispatcher'
import { Branch } from '../../models/branch'
import { Repository } from '../../models/repository'
import { Button } from '../lib/button'
import { ButtonGroup } from '../lib/button-group'
import { Dialog, DialogContent, DialogFooter } from '../dialog'
import { BranchList, IBranchListItem, renderDefaultBranch } from '../branches'
import { IMatches } from '../../lib/fuzzy-find'
import { truncateWithEllipsis } from '../../lib/truncate-with-ellipsis'
interface IRebaseBranchDialogProps {
readonly dispatcher: Dispatcher
readonly repository: Repository
/**
* See IBranchesState.defaultBranch
*/
readonly defaultBranch: Branch | null
/**
* The currently checked out branch
*/
readonly currentBranch: Branch
/**
* See IBranchesState.allBranches
*/
readonly allBranches: ReadonlyArray<Branch>
/**
* See IBranchesState.recentBranches
*/
readonly recentBranches: ReadonlyArray<Branch>
/**
* The branch to select when the rebase dialog is opened
*/
readonly initialBranch?: Branch
/**
* A function that's called when the dialog is dismissed by the user in the
* ways described in the Dialog component's dismissable prop.
*/
readonly onDismissed: () => void
}
interface IRebaseBranchDialogState {
/** The currently selected branch. */
readonly selectedBranch: Branch | null
/** The filter text to use in the branch selector */
readonly filterText: string
readonly isRebasing: boolean
}
/** A component for initating a rebase of the current branch. */
export class RebaseBranchDialog extends React.Component<
IRebaseBranchDialogProps,
IRebaseBranchDialogState
> {
public constructor(props: IRebaseBranchDialogProps) {
super(props)
const { initialBranch, currentBranch, defaultBranch } = props
const selectedBranch = resolveSelectedBranch(
currentBranch,
defaultBranch,
initialBranch
)
this.state = {
selectedBranch,
filterText: '',
isRebasing: false,
}
}
private onFilterTextChanged = (filterText: string) => {
this.setState({ filterText })
}
private onSelectionChanged = (selectedBranch: Branch | null) => {
this.setState({ selectedBranch })
}
private renderBranch = (item: IBranchListItem, matches: IMatches) => {
return renderDefaultBranch(item, matches, this.props.currentBranch)
}
public render() {
const { selectedBranch } = this.state
const { currentBranch } = this.props
const selectedBranchIsNotCurrentBranch =
selectedBranch === null ||
currentBranch === null ||
currentBranch.name === selectedBranch.name
const loading = this.state.isRebasing
const disabled = selectedBranchIsNotCurrentBranch || loading
const currentBranchName = currentBranch.name
// the amount of characters to allow before we truncate was chosen arbitrarily
const truncatedCurrentBranchName = truncateWithEllipsis(
currentBranchName,
40
)
return (
<Dialog
id="rebase"
onDismissed={this.props.onDismissed}
onSubmit={this.startRebase}
loading={loading}
disabled={disabled}
dismissable={true}
title={
<>
Rebase <strong>{truncatedCurrentBranchName}</strong> onto
</>
}
>
<DialogContent>
<BranchList
allBranches={this.props.allBranches}
currentBranch={currentBranch}
defaultBranch={this.props.defaultBranch}
recentBranches={this.props.recentBranches}
filterText={this.state.filterText}
onFilterTextChanged={this.onFilterTextChanged}
selectedBranch={selectedBranch}
onSelectionChanged={this.onSelectionChanged}
canCreateNewBranch={false}
renderBranch={this.renderBranch}
/>
</DialogContent>
<DialogFooter>
<ButtonGroup>
<Button type="submit">
Rebase <strong>{currentBranchName}</strong> onto{' '}
<strong>{selectedBranch ? selectedBranch.name : ''}</strong>
</Button>
</ButtonGroup>
</DialogFooter>
</Dialog>
)
}
private startRebase = async () => {
const branch = this.state.selectedBranch
if (!branch) {
return
}
// TODO: transition to a rebase progress dialog
this.setState({ isRebasing: true })
await this.props.dispatcher.rebase(
this.props.repository,
branch.name,
this.props.currentBranch.name
)
this.setState({ isRebasing: false })
this.props.onDismissed()
}
}
/**
* Returns the branch to use as the selected branch in the dialog.
*
* The initial branch is used if defined, otherwise the default branch will be
* compared to the current branch.
*
* If the current branch is the default branch, `null` is returned. Otherwise
* the default branch is used.
*/
function resolveSelectedBranch(
currentBranch: Branch,
defaultBranch: Branch | null,
initialBranch: Branch | undefined
) {
if (initialBranch !== undefined) {
return initialBranch
}
return currentBranch === defaultBranch ? null : defaultBranch
}

View file

@ -2,16 +2,26 @@ import * as React from 'react'
import { DialogContent, Dialog, DialogFooter } from '../dialog'
import { ButtonGroup } from '../lib/button-group'
import { Button } from '../lib/button'
import { DialogHeader } from '../dialog/header'
import { WorkingDirectoryStatus } from '../../models/status'
import { getUnmergedFiles, getConflictedFiles } from '../../lib/status'
import { ConflictedFilesList } from './conflict-files-list'
import {
WorkingDirectoryStatus,
WorkingDirectoryFileChange,
} from '../../models/status'
import {
getUnmergedFiles,
getConflictedFiles,
isConflictedFile,
} from '../../lib/status'
import { Dispatcher } from '../dispatcher'
import { Repository } from '../../models/repository'
import { ManualConflictResolution } from '../../models/manual-conflict-resolution'
import { ContinueRebaseResult } from '../../lib/git'
import { BannerType } from '../../models/banner'
import { PopupType } from '../../models/popup'
import {
renderUnmergedFilesSummary,
renderShellLink,
renderAllResolved,
} from '../lib/conflicts/render-functions'
import { renderUnmergedFile } from '../lib/conflicts/unmerged-file'
interface IRebaseConflictsDialog {
readonly dispatcher: Dispatcher
@ -54,14 +64,11 @@ export class RebaseConflictsDialog extends React.Component<
}
private onSubmit = async () => {
const result = await this.props.dispatcher.continueRebase(
await this.props.dispatcher.continueRebase(
this.props.repository,
this.props.workingDirectory
this.props.workingDirectory,
this.props.manualResolutions
)
if (result === ContinueRebaseResult.CompletedWithoutError) {
this.props.onDismissed()
}
}
private renderHeaderTitle(targetBranch: string, baseBranch?: string) {
@ -83,6 +90,50 @@ export class RebaseConflictsDialog extends React.Component<
)
}
private openThisRepositoryInShell = () =>
this.props.openRepositoryInShell(this.props.repository)
private renderUnmergedFiles(
files: ReadonlyArray<WorkingDirectoryFileChange>
) {
return (
<ul className="unmerged-file-statuses">
{files.map(f =>
isConflictedFile(f.status)
? renderUnmergedFile({
path: f.path,
status: f.status,
resolvedExternalEditor: this.props.resolvedExternalEditor,
openFileInExternalEditor: this.props.openFileInExternalEditor,
repository: this.props.repository,
dispatcher: this.props.dispatcher,
manualResolution: this.props.manualResolutions.get(f.path),
theirBranch: this.props.targetBranch,
ourBranch: this.props.baseBranch,
})
: null
)}
</ul>
)
}
private renderContent(
unmergedFiles: ReadonlyArray<WorkingDirectoryFileChange>,
conflictedFilesCount: number
): JSX.Element {
if (unmergedFiles.length === 0) {
return renderAllResolved()
}
return (
<>
{renderUnmergedFilesSummary(conflictedFilesCount)}
{this.renderUnmergedFiles(unmergedFiles)}
{renderShellLink(this.openThisRepositoryInShell)}
</>
)
}
public render() {
const unmergedFiles = getUnmergedFiles(this.props.workingDirectory)
const conflictedFilesCount = getConflictedFiles(
@ -105,22 +156,12 @@ export class RebaseConflictsDialog extends React.Component<
id="rebase-conflicts-list"
dismissable={true}
onDismissed={this.onDismissed}
title={headerTitle}
disableClickDismissalAlways={true}
onSubmit={this.onSubmit}
>
<DialogHeader
title={headerTitle}
dismissable={true}
onDismissed={this.onDismissed}
/>
<DialogContent>
<ConflictedFilesList
dispatcher={this.props.dispatcher}
repository={this.props.repository}
openFileInExternalEditor={this.props.openFileInExternalEditor}
resolvedExternalEditor={this.props.resolvedExternalEditor}
files={unmergedFiles}
/>
{this.renderContent(unmergedFiles, conflictedFilesCount)}
</DialogContent>
<DialogFooter>
<ButtonGroup>

View file

@ -23,6 +23,7 @@ interface IConfirmRemoveRepositoryProps {
interface IConfirmRemoveRepositoryState {
readonly deleteRepoFromDisk: boolean
readonly isRemovingRepository: boolean
}
export class ConfirmRemoveRepository extends React.Component<
@ -34,6 +35,7 @@ export class ConfirmRemoveRepository extends React.Component<
this.state = {
deleteRepoFromDisk: false,
isRemovingRepository: false,
}
}
@ -42,6 +44,8 @@ export class ConfirmRemoveRepository extends React.Component<
}
private onConfirmed = () => {
this.setState({ isRemovingRepository: true })
this.props.onConfirmation(
this.props.repository,
this.state.deleteRepoFromDisk
@ -51,12 +55,16 @@ export class ConfirmRemoveRepository extends React.Component<
}
public render() {
const isRemovingRepository = this.state.isRemovingRepository
return (
<Dialog
id="confirm-remove-repository"
key="remove-repository-confirmation"
type="warning"
title={__DARWIN__ ? 'Remove Repository' : 'Remove repository'}
dismissable={isRemovingRepository ? false : true}
loading={isRemovingRepository}
onDismissed={this.cancel}
onSubmit={this.cancel}
>
@ -86,8 +94,12 @@ export class ConfirmRemoveRepository extends React.Component<
</DialogContent>
<DialogFooter>
<ButtonGroup destructive={true}>
<Button type="submit">Cancel</Button>
<Button onClick={this.onConfirmed}>Remove</Button>
<Button disabled={isRemovingRepository} type="submit">
Cancel
</Button>
<Button onClick={this.onConfirmed} disabled={isRemovingRepository}>
Remove
</Button>
</ButtonGroup>
</DialogFooter>
</Dialog>

View file

@ -47,13 +47,32 @@ interface IPushPullButtonProps {
/** Is the detached HEAD state related to a rebase or not? */
readonly rebaseInProgress: boolean
/** If the current branch has been rebased, the user is permitted to force-push */
readonly branchWasRebased: boolean
}
/**
* This represents the "double arrow" icon used to show a force-push, and is a
* less complicated icon than the generated Octicon from the `octicons` package.
*/
const forcePushIcon = new OcticonSymbol(
10,
16,
'M3 11H0l5-6 5 6H7v4H3v-4zM5 1l5 6H8.33L5 3 1.662 7H0l5-6z'
)
function getActionLabel(
{ ahead, behind }: IAheadBehind,
aheadBehind: IAheadBehind,
remoteName: string,
branchWasRebased: boolean,
pullWithRebase?: boolean
) {
if (isBranchRebased(branchWasRebased, aheadBehind)) {
return `Force push ${remoteName}`
}
const { ahead, behind } = aheadBehind
if (behind > 0) {
return pullWithRebase && enablePullWithRebase()
? `Pull ${remoteName} with rebase`
@ -65,6 +84,10 @@ function getActionLabel(
return `Fetch ${remoteName}`
}
function isBranchRebased(branchWasRebased: boolean, aheadBehind: IAheadBehind) {
return branchWasRebased && aheadBehind.behind > 0 && aheadBehind.ahead > 0
}
/**
* A button which pushes, pulls, or updates depending on the state of the
* repository.
@ -157,6 +180,7 @@ export class PushPullButton extends React.Component<IPushPullButtonProps, {}> {
return getActionLabel(
this.props.aheadBehind,
this.props.remoteName,
this.props.branchWasRebased,
this.props.pullWithRebase
)
}
@ -177,6 +201,11 @@ export class PushPullButton extends React.Component<IPushPullButtonProps, {}> {
if (this.props.networkActionInProgress) {
return OcticonSymbol.sync
}
if (this.props.branchWasRebased) {
return forcePushIcon
}
if (behind > 0) {
return OcticonSymbol.arrowDown
}
@ -221,9 +250,7 @@ export class PushPullButton extends React.Component<IPushPullButtonProps, {}> {
}
private performAction = () => {
const repository = this.props.repository
const dispatcher = this.props.dispatcher
const aheadBehind = this.props.aheadBehind
const { repository, dispatcher, aheadBehind, branchWasRebased } = this.props
if (!aheadBehind) {
dispatcher.push(repository)
@ -232,7 +259,9 @@ export class PushPullButton extends React.Component<IPushPullButtonProps, {}> {
const { ahead, behind } = aheadBehind
if (behind > 0) {
if (isBranchRebased(branchWasRebased, aheadBehind)) {
dispatcher.confirmOrForcePush(repository)
} else if (behind > 0) {
dispatcher.pull(repository)
} else if (ahead > 0) {
dispatcher.push(repository)

View file

@ -78,10 +78,10 @@
height: 100%;
width: 100%;
.filter-list-filter-field {
&.filter-list .filter-field-row {
// The rows have built-in margin to their content so
// we only need half a spacer here
padding-bottom: var(--spacing-half);
margin-bottom: var(--spacing-half);
}
.list-item.selected:focus {

View file

@ -1,6 +1,7 @@
@import '../../mixins';
dialog#merge-conflicts-list {
dialog#merge-conflicts-list,
dialog#rebase-conflicts-list {
width: 500px;
.summary {

View file

@ -0,0 +1,115 @@
@import '../../mixins';
dialog#rebase-conflicts-list {
width: 500px;
.summary {
margin-bottom: 20px;
}
.dialog-header h1 {
font-weight: var(--font-weight-light);
}
.dialog-content {
padding-bottom: var(--spacing);
}
.green-circle {
background-color: var(--color-new);
color: var(--background-color);
border-radius: 50%;
height: 22px;
width: 22px;
display: flex;
justify-content: center;
align-items: center;
}
ul {
list-style: none;
padding: 0;
max-height: 285px;
overflow-y: auto;
li {
margin-bottom: var(--spacing-double);
padding-left: 0;
}
li:last-of-type {
margin-bottom: calc(var(--spacing) * 2);
}
li.unmerged-file-status-resolved,
li.unmerged-file-status-conflicts {
display: flex;
flex-flow: row nowrap;
.file-octicon {
margin-right: var(--spacing);
}
.column-left {
display: flex;
flex-flow: column nowrap;
align-items: start;
max-width: 50%;
padding-right: var(--spacing);
.path-text-component {
text-overflow: ellipsis;
max-width: 100%;
}
}
.action-buttons {
margin-left: auto;
flex-shrink: 0;
flex: 0 1 auto;
display: flex;
align-items: center;
justify-content: center;
}
.green-circle:last-child {
margin-left: auto;
margin-top: var(--spacing);
flex-shrink: 0;
flex: 0 1 auto;
}
}
.unmerged-file-status-resolved .file-conflicts-status {
color: var(--color-new);
}
.unmerged-file-status-conflicts {
.file-conflicts-status {
color: var(--color-conflicted);
}
.command-line-hint {
color: var(--text-secondary-color);
}
}
}
.all-conflicts-resolved {
display: flex;
flex-flow: row nowrap;
padding: var(--spacing) 0 var(--spacing-double);
.message {
padding-left: var(--spacing);
padding-top: var(--spacing-third);
}
}
.arrow-menu {
.octicon {
width: 10px;
height: 14px;
}
}
}

View file

@ -1,124 +1,51 @@
@import '../../mixins';
dialog#rebase-conflicts-list {
width: 500px;
dialog#rebase {
width: 450px;
.summary {
margin-bottom: 20px;
.branches-list {
height: 300px;
}
.dialog-header h1 {
// We're faking it so that the filter text box appears to reside
// withing the header even though our current component structure
// make it extremely hard to do so.
.dialog-header {
border-bottom: none;
h1 {
font-weight: var(--font-weight-light);
}
}
.dialog-content {
padding: 0;
.filter-field-row {
margin: 0;
border-bottom: var(--base-border);
.filter-list-filter-field {
padding: 0 var(--spacing-double);
padding-bottom: var(--spacing);
}
.green-circle {
background-color: var(--color-new);
color: var(--background-color);
border-radius: 50%;
height: 22px;
width: 22px;
display: flex;
justify-content: center;
align-items: center;
}
ul {
list-style: none;
.list-item {
padding: 0 var(--spacing-double);
.filter-list-group-header,
.branches-list-item {
padding: 0;
max-height: 285px;
overflow-y: auto;
li {
margin-bottom: var(--spacing-double);
padding-left: 0;
}
li:last-of-type {
margin-bottom: calc(var(--spacing) * 2);
}
li.unmerged-file-status-resolved,
li.unmerged-file-status-conflicts {
display: flex;
flex-flow: row nowrap;
.file-octicon {
margin-right: var(--spacing);
}
.column-left {
display: flex;
flex-flow: column nowrap;
align-items: start;
max-width: 50%;
padding-right: var(--spacing);
.path-text-component {
text-overflow: ellipsis;
max-width: 100%;
}
}
.action-buttons {
margin-left: auto;
flex-shrink: 0;
flex: 0 1 auto;
display: flex;
align-items: center;
justify-content: center;
}
.green-circle:last-child {
margin-left: auto;
margin-top: var(--spacing);
flex-shrink: 0;
flex: 0 1 auto;
}
}
.unmerged-file-status-resolved .file-conflicts-status {
color: $green;
}
.unmerged-file-status-conflicts {
.file-conflicts-status {
color: $orange;
}
.command-line-hint {
color: $gray;
}
}
}
.all-conflicts-resolved {
display: flex;
flex-flow: row nowrap;
padding: var(--spacing) 0 var(--spacing-double);
.message {
padding-left: var(--spacing);
padding-top: var(--spacing-third);
}
}
.cli-link {
a {
color: $blue;
&:hover {
text-decoration: underline;
}
}
}
.arrow-menu {
.octicon {
width: 10px;
.dialog-footer {
button[type='submit'] {
height: auto;
width: 100%;
padding: var(--spacing-half);
}
}
}

View file

@ -0,0 +1,11 @@
import { Repository } from '../../src/models/repository'
import { GitProcess } from 'dugite'
export async function setupLocalConfig(
repository: Repository,
localConfig: Iterable<[string, string]>
) {
for (const [key, value] of localConfig) {
await GitProcess.exec(['config', key, value], repository.path)
}
}

View file

@ -40,6 +40,10 @@ export async function createRepository(
path: 'OTHER.md',
contents: '# HELLO WORLD! \nTHINGS GO HERE\n',
},
{
path: 'THIRD.md',
contents: 'nothing goes here',
},
],
}

View file

@ -210,8 +210,6 @@ async function initializeTestRepo(
url: '',
login: '',
avatar_url: '',
name: null,
email: null,
type: 'User',
},
private: false,

View file

@ -677,5 +677,41 @@ describe('git/commit', () => {
expect(commit!.summary).toEqual('commit again!')
expect(commit!.shortSha).toEqual(sha)
})
it('file is deleted in index', async () => {
const repo = await setupEmptyRepository()
await FSE.writeFile(path.join(repo.path, 'secret'), 'contents\n')
await FSE.writeFile(path.join(repo.path, '.gitignore'), '')
// Setup repo to reproduce bug
await GitProcess.exec(['add', '.'], repo.path)
await GitProcess.exec(['commit', '-m', 'Initial commit'], repo.path)
// Make changes that should remain secret
await FSE.writeFile(path.join(repo.path, 'secret'), 'Somethign secret\n')
// Ignore it
await FSE.writeFile(path.join(repo.path, '.gitignore'), 'secret')
// Remove from index to mark as deleted
await GitProcess.exec(['rm', '--cached', 'secret'], repo.path)
// Make sure that file is marked as deleted
const beforeCommit = await getStatusOrThrow(repo)
const files = beforeCommit.workingDirectory.files
expect(files.length).toBe(2)
expect(files[1].status.kind).toBe(AppFileStatusKind.Deleted)
// Commit changes
await createCommit(repo!, 'FAIL commit', files)
const afterCommit = await getStatusOrThrow(repo)
expect(beforeCommit.currentTip).not.toBe(afterCommit.currentTip)
// Verify the file was delete in repo
const changedFiles = await getChangedFiles(repo, afterCommit.currentTip!)
expect(changedFiles.length).toBe(2)
expect(changedFiles[0].status.kind).toBe(AppFileStatusKind.Modified)
expect(changedFiles[1].status.kind).toBe(AppFileStatusKind.Deleted)
})
})
})

View file

@ -348,6 +348,7 @@ describe('git/diff', () => {
repo.path
)
// change config on-the-fly to trigger the line endings change warning
await GitProcess.exec(['config', 'core.autocrlf', 'true'], repo.path)
lineEnding = '\n\n'

View file

@ -9,6 +9,7 @@ import {
readGitIgnoreAtRoot,
appendIgnoreRule,
} from '../../../src/lib/git'
import { setupLocalConfig } from '../../helpers/local-config'
describe('gitignore', () => {
describe('readGitIgnoreAtRoot', () => {
@ -37,16 +38,12 @@ describe('gitignore', () => {
it('when autocrlf=true and safecrlf=true, appends CRLF to file', async () => {
const repo = await setupEmptyRepository()
await GitProcess.exec(
['config', '--local', 'core.autocrlf', 'true'],
repo.path
)
await GitProcess.exec(
['config', '--local', 'core.safecrlf', 'true'],
repo.path
)
await setupLocalConfig(repo, [
['core.autocrlf', 'true'],
['core.safecrlf', 'true'],
])
const path = repo.path
const { path } = repo
await saveGitIgnore(repo, 'node_modules')
await GitProcess.exec(['add', '.gitignore'], path)
@ -64,16 +61,14 @@ describe('gitignore', () => {
it('when autocrlf=input, appends LF to file', async () => {
const repo = await setupEmptyRepository()
setupLocalConfig(repo, [
// ensure this repository only ever sticks to LF
await GitProcess.exec(['config', '--local', 'core.eol', 'lf'], repo.path)
['core.eol', 'lf'],
// do not do any conversion of line endings when committing
await GitProcess.exec(
['config', '--local', 'core.autocrlf', 'input'],
repo.path
)
['core.autocrlf', 'input'],
])
const path = repo.path
const { path } = repo
await saveGitIgnore(repo, 'node_modules')
await GitProcess.exec(['add', '.gitignore'], path)
@ -139,12 +134,10 @@ describe('gitignore', () => {
describe('appendIgnoreRule', () => {
it('appends one rule', async () => {
const repo = await setupEmptyRepository()
const path = repo.path
await GitProcess.exec(
['config', '--local', 'core.autocrlf', 'true'],
path
)
await setupLocalConfig(repo, [['core.autocrlf', 'true']])
const { path } = repo
const ignoreFile = `${path}/.gitignore`
await FSE.writeFile(ignoreFile, 'node_modules\n')
@ -159,12 +152,10 @@ describe('gitignore', () => {
it('appends multiple rules', async () => {
const repo = await setupEmptyRepository()
const path = repo.path
await GitProcess.exec(
['config', '--local', 'core.autocrlf', 'true'],
path
)
await setupLocalConfig(repo, [['core.autocrlf', 'true']])
const { path } = repo
const ignoreFile = `${path}/.gitignore`
await FSE.writeFile(ignoreFile, 'node_modules\n')

View file

@ -2,7 +2,7 @@ import { Repository } from '../../../src/models/repository'
import { getChangedFiles, getCommits } from '../../../src/lib/git'
import { setupFixtureRepository } from '../../helpers/repositories'
import { AppFileStatusKind } from '../../../src/models/status'
import { GitProcess } from 'dugite'
import { setupLocalConfig } from '../../helpers/local-config'
describe('git/log', () => {
let repository: Repository | null = null
@ -34,11 +34,10 @@ describe('git/log', () => {
const path = await setupFixtureRepository('just-doing-some-signing')
const repository = new Repository(path, 1, null, false)
// ensure the test repository is configured to detect copies
await GitProcess.exec(
['config', 'log.showSignature', 'true'],
repository.path
)
// ensure the default config is to try and show signatures
// this should be overriden by the `getCommits` function as it may not
// have a valid GPG agent configured
await setupLocalConfig(repository, [['log.showSignature', 'true']])
const commits = await getCommits(repository, 'HEAD', 100)
@ -91,10 +90,7 @@ describe('git/log', () => {
repository = new Repository(testRepoPath, -1, null, false)
// ensure the test repository is configured to detect copies
await GitProcess.exec(
['config', 'diff.renames', 'copies'],
repository.path
)
await setupLocalConfig(repository, [['diff.renames', 'copies']])
const files = await getChangedFiles(repository, 'a500bf415')
expect(files).toHaveLength(2)

View file

@ -12,7 +12,7 @@ import {
makeCommit,
} from '../../../helpers/repository-scaffolding'
import { getTipOrError, getRefOrError } from '../../../helpers/tip'
import { GitProcess } from 'dugite'
import { setupLocalConfig } from '../../../helpers/local-config'
const featureBranch = 'this-is-a-feature'
const origin = 'origin'
@ -54,11 +54,16 @@ describe('git/pull', () => {
await fetch(repository, null, origin)
})
describe('by default', () => {
describe('with pull.rebase=false and pull.ff=false set in config', () => {
let previousTip: Commit
let newTip: Commit
beforeEach(async () => {
await setupLocalConfig(repository, [
['pull.rebase', 'false'],
['pull.ff', 'false'],
])
previousTip = await getTipOrError(repository)
await pull(repository, null, origin)
@ -67,8 +72,6 @@ describe('git/pull', () => {
})
it('creates a merge commit', async () => {
const newTip = await getTipOrError(repository)
expect(newTip.sha).not.toBe(previousTip.sha)
expect(newTip.parentSHAs).toHaveLength(2)
})
@ -92,51 +95,12 @@ describe('git/pull', () => {
})
})
describe('with pull.ff=false set in config', () => {
let previousTip: Commit
let newTip: Commit
beforeEach(async () => {
await GitProcess.exec(
['config', '--local', 'pull.ff', 'false'],
repository.path
)
previousTip = await getTipOrError(repository)
await pull(repository, null, origin)
newTip = await getTipOrError(repository)
})
it('creates a merge commit', async () => {
expect(newTip.sha).not.toBe(previousTip.sha)
expect(newTip.parentSHAs).toHaveLength(2)
})
it('is ahead of tracking branch', async () => {
const range = revSymmetricDifference(
featureBranch,
`${origin}/${featureBranch}`
)
const aheadBehind = await getAheadBehind(repository, range)
expect(aheadBehind).toEqual({
ahead: 2,
behind: 0,
})
})
})
describe('with pull.rebase=false set in config', () => {
let previousTip: Commit
let newTip: Commit
beforeEach(async () => {
await GitProcess.exec(
['config', '--local', 'pull.rebase', 'false'],
repository.path
)
await setupLocalConfig(repository, [['pull.rebase', 'false']])
previousTip = await getTipOrError(repository)
@ -169,10 +133,7 @@ describe('git/pull', () => {
let newTip: Commit
beforeEach(async () => {
await GitProcess.exec(
['config', '--local', 'pull.rebase', 'true'],
repository.path
)
await setupLocalConfig(repository, [['pull.rebase', 'true']])
previousTip = await getTipOrError(repository)
@ -200,12 +161,12 @@ describe('git/pull', () => {
})
})
describe('with pull.ff=only set in config', () => {
describe('with pull.rebase=false and pull.ff=only set in config', () => {
beforeEach(async () => {
await GitProcess.exec(
['config', '--local', 'pull.ff', 'only'],
repository.path
)
await setupLocalConfig(repository, [
['pull.rebase', 'false'],
['pull.ff', 'only'],
])
})
it(`throws an error as the user blocks merge commits on pull`, () => {

View file

@ -12,7 +12,7 @@ import {
makeCommit,
} from '../../../helpers/repository-scaffolding'
import { getTipOrError, getRefOrError } from '../../../helpers/tip'
import { GitProcess } from 'dugite'
import { setupLocalConfig } from '../../../helpers/local-config'
const featureBranch = 'this-is-a-feature'
const origin = 'origin'
@ -80,10 +80,8 @@ describe('git/pull', () => {
let newTip: Commit
beforeEach(async () => {
await GitProcess.exec(
['config', '--local', 'pull.ff', 'false'],
repository.path
)
await setupLocalConfig(repository, [['pull.ff', 'false']])
previousTip = await getTipOrError(repository)
await pull(repository, null, origin)
@ -116,10 +114,7 @@ describe('git/pull', () => {
let newTip: Commit
beforeEach(async () => {
await GitProcess.exec(
['config', '--local', 'pull.ff', 'only'],
repository.path
)
await setupLocalConfig(repository, [['pull.ff', 'only']])
previousTip = await getTipOrError(repository)

View file

@ -12,7 +12,7 @@ import {
makeCommit,
} from '../../../helpers/repository-scaffolding'
import { getTipOrError, getRefOrError } from '../../../helpers/tip'
import { GitProcess } from 'dugite'
import { setupLocalConfig } from '../../../helpers/local-config'
const featureBranch = 'this-is-a-feature'
const origin = 'origin'
@ -54,50 +54,15 @@ describe('git/pull', () => {
await fetch(repository, null, origin)
})
describe('by default', () => {
describe('with pull.rebase=false and pull.ff=false set in config', () => {
let previousTip: Commit
let newTip: Commit
beforeEach(async () => {
previousTip = await getTipOrError(repository)
await pull(repository, null, origin)
newTip = await getTipOrError(repository)
})
it('moves local repository to remote commit', async () => {
const newTip = await getTipOrError(repository)
expect(newTip.sha).not.toBe(previousTip.sha)
expect(newTip.parentSHAs).toHaveLength(1)
})
it('is same as remote branch', async () => {
const remoteCommit = await getRefOrError(repository, remoteBranch)
expect(remoteCommit.sha).toBe(newTip.sha)
})
it('is not behind tracking branch', async () => {
const range = revSymmetricDifference(featureBranch, remoteBranch)
const aheadBehind = await getAheadBehind(repository, range)
expect(aheadBehind).toEqual({
ahead: 0,
behind: 0,
})
})
})
describe('with pull.ff=false set in config', () => {
let previousTip: Commit
let newTip: Commit
beforeEach(async () => {
await GitProcess.exec(
['config', '--local', 'pull.ff', 'false'],
repository.path
)
await setupLocalConfig(repository, [
['pull.rebase', 'false'],
['pull.ff', 'false'],
])
previousTip = await getTipOrError(repository)
@ -132,10 +97,7 @@ describe('git/pull', () => {
let newTip: Commit
beforeEach(async () => {
await GitProcess.exec(
['config', '--local', 'pull.ff', 'only'],
repository.path
)
await setupLocalConfig(repository, [['pull.ff', 'only']])
previousTip = await getTipOrError(repository)

View file

@ -1,16 +1,19 @@
import { IGitResult, GitProcess } from 'dugite'
import { GitProcess } from 'dugite'
import * as FSE from 'fs-extra'
import * as Path from 'path'
import { IStatusResult } from '../../../../src/lib/git'
import { IStatusResult, getChangedFiles } from '../../../../src/lib/git'
import {
abortRebase,
continueRebase,
rebase,
ContinueRebaseResult,
RebaseResult,
} from '../../../../src/lib/git/rebase'
import { Commit } from '../../../../src/models/commit'
import { AppFileStatusKind } from '../../../../src/models/status'
import {
AppFileStatusKind,
CommittedFileChange,
} from '../../../../src/models/status'
import { createRepository } from '../../../helpers/repository-builder-rebase-test'
import { getStatusOrThrow } from '../../../helpers/status'
import { getRefOrError } from '../../../helpers/tip'
@ -20,7 +23,7 @@ const featureBranch = 'this-is-a-feature'
describe('git/rebase', () => {
describe('detect conflicts', () => {
let result: IGitResult
let result: RebaseResult
let originalBranchTip: string
let baseBranchTip: string
let status: IStatusResult
@ -39,8 +42,8 @@ describe('git/rebase', () => {
status = await getStatusOrThrow(repository)
})
it('returns a non-zero exit code', async () => {
expect(result.exitCode).toBeGreaterThan(0)
it('returns a value indicating conflicts were encountered', async () => {
expect(result).toBe(RebaseResult.ConflictsEncountered)
})
it('status detects REBASE_HEAD', async () => {
@ -91,7 +94,7 @@ describe('git/rebase', () => {
})
describe('attempt to continue without resolving conflicts', () => {
let result: ContinueRebaseResult
let result: RebaseResult
let originalBranchTip: string
let baseBranchTip: string
let status: IStatusResult
@ -116,7 +119,7 @@ describe('git/rebase', () => {
})
it('indicates that the rebase was not complete', async () => {
expect(result).toBe(ContinueRebaseResult.OutstandingFilesNotStaged)
expect(result).toBe(RebaseResult.OutstandingFilesNotStaged)
})
it('REBASE_HEAD is still found', async () => {
@ -138,7 +141,7 @@ describe('git/rebase', () => {
describe('continue after resolving conflicts', () => {
let beforeRebaseTip: Commit
let result: ContinueRebaseResult
let result: RebaseResult
let status: IStatusResult
beforeEach(async () => {
@ -183,7 +186,7 @@ describe('git/rebase', () => {
})
it('returns success', () => {
expect(result).toBe(ContinueRebaseResult.CompletedWithoutError)
expect(result).toBe(RebaseResult.CompletedWithoutError)
})
it('REBASE_HEAD is no longer found', () => {
@ -202,4 +205,118 @@ describe('git/rebase', () => {
expect(status.currentTip).not.toBe(beforeRebaseTip.sha)
})
})
describe('continue with additional changes unrelated to conflicted files', () => {
let beforeRebaseTip: Commit
let filesInRebasedCommit: ReadonlyArray<CommittedFileChange>
let result: RebaseResult
let status: IStatusResult
beforeEach(async () => {
const repository = await createRepository(baseBranch, featureBranch)
beforeRebaseTip = await getRefOrError(repository, featureBranch)
await rebase(repository, baseBranch, featureBranch)
// resolve conflicts by writing files to disk
await FSE.writeFile(
Path.join(repository.path, 'THING.md'),
'# HELLO WORLD! \nTHINGS GO HERE\nFEATURE BRANCH UNDERWAY\n'
)
await FSE.writeFile(
Path.join(repository.path, 'OTHER.md'),
'# HELLO WORLD! \nTHINGS GO HERE\nALSO FEATURE BRANCH UNDERWAY\n'
)
// change unrelated tracked while rebasing changes
await FSE.writeFile(
Path.join(repository.path, 'THIRD.md'),
'this change should be included in the latest commit'
)
// add untracked file before continuing rebase
await FSE.writeFile(
Path.join(repository.path, 'UNTRACKED-FILE.md'),
'this file should remain in the working directory'
)
const afterRebase = await getStatusOrThrow(repository)
const { files } = afterRebase.workingDirectory
result = await continueRebase(repository, files)
status = await getStatusOrThrow(repository)
filesInRebasedCommit = await getChangedFiles(
repository,
status.currentTip!
)
})
it('returns success', () => {
expect(result).toBe(RebaseResult.CompletedWithoutError)
})
it('keeps untracked working directory file out of rebase', () => {
expect(status.workingDirectory.files).toHaveLength(1)
})
it('has modified but unconflicted file in commit contents', () => {
expect(
filesInRebasedCommit.find(f => f.path === 'THIRD.md')
).not.toBeUndefined()
})
it('returns to the feature branch', () => {
expect(status.currentBranch).toBe(featureBranch)
})
it('branch is now a different ref', () => {
expect(status.currentTip).not.toBe(beforeRebaseTip.sha)
})
})
describe('continue with tracked change omitted from list', () => {
let result: RebaseResult
beforeEach(async () => {
const repository = await createRepository(baseBranch, featureBranch)
await rebase(repository, baseBranch, featureBranch)
// resolve conflicts by writing files to disk
await FSE.writeFile(
Path.join(repository.path, 'THING.md'),
'# HELLO WORLD! \nTHINGS GO HERE\nFEATURE BRANCH UNDERWAY\n'
)
await FSE.writeFile(
Path.join(repository.path, 'OTHER.md'),
'# HELLO WORLD! \nTHINGS GO HERE\nALSO FEATURE BRANCH UNDERWAY\n'
)
// change unrelated tracked while rebasing changes
await FSE.writeFile(
Path.join(repository.path, 'THIRD.md'),
'this change should be included in the latest commit'
)
const afterRebase = await getStatusOrThrow(repository)
const { files } = afterRebase.workingDirectory
// omit the last change should cause Git to error because it requires
// all tracked changes to be staged as a prerequisite for rebasing
const onlyConflictedFiles = files.filter(f => f.path !== 'THIRD.md')
result = await continueRebase(repository, onlyConflictedFiles)
})
it('returns error code indicating that required files were missing', () => {
expect(result).toBe(RebaseResult.OutstandingFilesNotStaged)
})
})
})

View file

@ -63,6 +63,11 @@ describe('git/remote', () => {
})
describe('findDefaultRemote', () => {
it('returns null for empty array', async () => {
const result = await findDefaultRemote([])
expect(result).toBeNull()
})
it('returns origin when multiple remotes found', async () => {
const testRepoPath = await setupFixtureRepository(
'repo-with-multiple-remotes'

View file

@ -0,0 +1,73 @@
import * as FSE from 'fs-extra'
import * as path from 'path'
import { Repository } from '../../../src/models/repository'
import { setupEmptyRepository } from '../../helpers/repositories'
import { GitProcess } from 'dugite'
import {
getDesktopStashEntries,
createStashMessage,
} from '../../../src/lib/git/stash'
describe('git/stash', () => {
describe('getDesktopStashEntries', () => {
let repository: Repository
let readme: string
beforeEach(async () => {
repository = await setupEmptyRepository()
readme = path.join(repository.path, 'README.md')
await FSE.writeFile(readme, '')
await GitProcess.exec(['add', 'README.md'], repository.path)
await GitProcess.exec(['commit', '-m', 'initial commit'], repository.path)
})
it('handles unborn repo by returning empty list', async () => {
const repo = await setupEmptyRepository()
let didFail = false
readme = path.join(repo.path, 'README.md')
await FSE.writeFile(readme, '')
await stash(repo)
try {
await getDesktopStashEntries(repo)
} catch (e) {
didFail = true
}
expect(didFail).toBe(true)
})
it('returns all stash entries created by Desktop', async () => {
await generateTestStashEntries(repository)
const stashEntries = await getDesktopStashEntries(repository)
expect(stashEntries).toHaveLength(1)
expect(stashEntries[0].branchName).toBe('master')
})
})
})
async function stash(repository: Repository, message?: string) {
const result = await GitProcess.exec(['rev-parse', 'HEAD'], repository.path)
const tipSha = result.stdout.trim()
await GitProcess.exec(
['stash', 'push', '-m', message || createStashMessage('master', tipSha)],
repository.path
)
}
async function generateTestStashEntries(repository: Repository) {
const readme = path.join(repository.path, 'README.md')
// simulate stashing from CLI
await FSE.appendFile(readme, '1')
await stash(repository, 'should get filtered')
await FSE.appendFile(readme, '2')
await stash(repository, 'should also get filtered')
// simulate stashing from Desktop
await FSE.appendFile(readme, '2')
await stash(repository)
}

View file

@ -20,6 +20,7 @@ import {
import * as temp from 'temp'
import { getStatus } from '../../../src/lib/git'
import { isConflictedFile } from '../../../src/lib/status'
import { setupLocalConfig } from '../../helpers/local-config'
const _temp = temp.track()
const mkdir = _temp.mkdir
@ -229,10 +230,7 @@ describe('git/status', () => {
// Git 2.18 now uses a new config value to handle detecting copies, so
// users who have this enabled will see this. For reference, Desktop does
// not enable this by default.
await GitProcess.exec(
['config', '--local', 'status.renames', 'copies'],
repository.path
)
await setupLocalConfig(repository, [['status.renames', 'copies']])
await GitProcess.exec(['add', '.'], repository.path)

View file

@ -2,7 +2,7 @@ import {
groupRepositories,
YourRepositoriesIdentifier,
} from '../../src/ui/clone-repository/group-repositories'
import { IAPIRepository, IAPIUser } from '../../src/lib/api'
import { IAPIRepository, IAPIIdentity } from '../../src/lib/api'
const users = {
shiftkey: {
@ -12,7 +12,7 @@ const users = {
avatar_url: '',
name: 'Brendan Forster',
type: 'User',
} as IAPIUser,
} as IAPIIdentity,
desktop: {
id: 2,
url: '',
@ -20,7 +20,7 @@ const users = {
avatar_url: '',
name: 'Desktop',
type: 'Organization',
} as IAPIUser,
} as IAPIIdentity,
octokit: {
id: 3,
url: '',
@ -28,7 +28,7 @@ const users = {
avatar_url: '',
name: 'Octokit',
type: 'Organization',
} as IAPIUser,
} as IAPIIdentity,
}
describe('clone repository grouping', () => {

View file

@ -43,8 +43,6 @@ describe('RepositoriesStore', () => {
url: 'https://github.com/my-user',
login: 'my-user',
avatar_url: 'https://github.com/my-user.png',
email: 'my-user@users.noreply.github.com',
name: 'My User',
type: 'User',
},
private: true,

View file

@ -215,17 +215,15 @@ code-point-at@^1.0.0:
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
codemirror-mode-elixir@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/codemirror-mode-elixir/-/codemirror-mode-elixir-1.1.1.tgz#cc5b79bf5f93b6da426e32364a673a681391416c"
integrity sha1-zFt5v1+TttpCbjI2Smc6aBORQWw=
dependencies:
codemirror "^5.20.2"
codemirror-mode-elixir@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/codemirror-mode-elixir/-/codemirror-mode-elixir-1.1.2.tgz#61227208d2684d928500af6934e4b9c995fb0960"
integrity sha512-1oIuRVHyUhLv0Za9sEIsI7urAj06EohwO/yVj10bg7aHnimHQ964Wk3uuoPH0Yn8L38EkOd+SwULYpDiCQtoTA==
codemirror@^5.20.2, codemirror@^5.31.0:
version "5.33.0"
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.33.0.tgz#462ad9a6fe8d38b541a9536a3997e1ef93b40c6a"
integrity sha512-HT6PKVqkwpzwB3jl5hXFoQteEWXbSWMzG3Z8RVYlx8hZwCOLCy4NU7vkSB3dYX3e6ORwRfGw4uFOXaw4rn/a9Q==
codemirror@^5.44.0:
version "5.44.0"
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.44.0.tgz#80dc2a231eeb7aab25ec2405cdca37e693ccf9cc"
integrity sha512-3l42syTNakCdCQuYeZJXTyxina6Y9i4V0ighSJXNCQtRbaCN76smKKLu1ZHPHQon3rnzC7l4i/0r4gp809K1wg==
color-convert@^1.9.0:
version "1.9.0"
@ -404,14 +402,14 @@ double-ended-queue@^2.1.0-0:
resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c"
integrity sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=
dugite@1.80.0:
version "1.80.0"
resolved "https://registry.yarnpkg.com/dugite/-/dugite-1.80.0.tgz#69987fd54d8afd8d56b2e0f4fe43f2db1b49d800"
integrity sha512-ELavYXdThykKFffPG0w4NdfWYr5FfwKOP4lmLpnripbuJOE8DTTvgEJigG4dC2Vkpe5lavWX2Oia9qQoEyMzeQ==
dugite@1.85.0:
version "1.85.0"
resolved "https://registry.yarnpkg.com/dugite/-/dugite-1.85.0.tgz#05cff1f0d4952cf32d1d6a30f4f380811e4583f5"
integrity sha512-33YIKzzuSIoB8cRDrIPozvvIJiPs4+XQJGcb9g48O99Q0GkPb1ipy3YjknJTTU0TwTB+NyGjA6m1jBRoR3XvuQ==
dependencies:
checksum "^0.1.1"
mkdirp "^0.5.1"
progress "^2.0.0"
progress "^2.0.3"
request "^2.88.0"
rimraf "^2.5.4"
tar "^4.4.7"
@ -859,13 +857,13 @@ keyboardevents-areequal@^0.2.1:
resolved "https://registry.yarnpkg.com/keyboardevents-areequal/-/keyboardevents-areequal-0.2.2.tgz#88191ec738ce9f7591c25e9056de928b40277194"
integrity sha512-Nv+Kr33T0mEjxR500q+I6IWisOQ0lK1GGOncV0kWE6n4KFmpcu7RUX5/2B0EUtX51Cb0HjZ9VJsSY3u4cBa0kw==
keytar@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/keytar/-/keytar-4.3.0.tgz#4a3afd64fdeec300716ccf3985fdcf1cfd6e77e9"
integrity sha512-pd++/v+fS0LQKmzWlW6R1lziTXFqhfGeS6sYLfuTIqEy2pDzAbjutbSW8f9tnJdEEMn/9XhAQlT34VAtl9h4MQ==
keytar@^4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/keytar/-/keytar-4.4.1.tgz#156af8a9b016bf118ee9948b02418c81d760a5ba"
integrity sha512-6xEe7ybXSR5EZC+z0GI2yqLYZjV1tyPQY2xSZ8rGsBxrrLEh8VR/Lfqv59uGX+I+W+OZxH0jCXN1dU1++ify4g==
dependencies:
nan "2.8.0"
prebuild-install "^5.0.0"
nan "2.12.1"
prebuild-install "5.2.4"
lodash._baseassign@^3.0.0:
version "3.2.0"
@ -1031,10 +1029,10 @@ mocha@^3.5.0:
mkdirp "0.5.1"
supports-color "3.1.2"
moment@^2.17.1:
version "2.18.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
integrity sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8=
moment@^2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
mri@^1.1.0:
version "1.1.0"
@ -1046,24 +1044,29 @@ ms@2.0.0:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
nan@2.8.0, nan@^2.8.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a"
integrity sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=
nan@2.12.1:
version "2.12.1"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552"
integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==
nan@^2.10.0:
version "2.11.1"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.1.tgz#90e22bccb8ca57ea4cd37cc83d3819b52eea6766"
nan@^2.8.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a"
integrity sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=
napi-build-utils@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.1.tgz#1381a0f92c39d66bf19852e7873432fc2123e508"
integrity sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA==
node-abi@^2.2.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.5.0.tgz#942e1a78bce764bc0c1672d5821e492b9d032052"
integrity sha512-9g2twBGSP6wIR5PW7tXvAWnEWKJDH/VskdXp168xsw9VVxpEGov8K4jsP4/VeoC7b2ZAyzckvMCuQuQlw44lXg==
node-abi@^2.7.0:
version "2.7.1"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.7.1.tgz#a8997ae91176a5fbaa455b194976e32683cda643"
integrity sha512-OV8Bq1OrPh6z+Y4dqwo05HqrRL9YNF7QVMRfq1/pguwKLG+q9UB/Lk0x5qXjO23JjJg+/jqCHSTaG1P3tfKfuw==
dependencies:
semver "^5.4.1"
@ -1151,10 +1154,10 @@ performance-now@^2.1.0:
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
prebuild-install@^5.0.0:
version "5.2.2"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.2.2.tgz#237888f21bfda441d0ee5f5612484390bccd4046"
integrity sha512-4e8VJnP3zJdZv/uP0eNWmr2r9urp4NECw7Mt1OSAi3rcLrbBRxGiAkfUFtre2MhQ5wfREAjRV+K1gubvs/GPsA==
prebuild-install@5.2.4:
version "5.2.4"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.2.4.tgz#8cc41a217ef778a31d3a876fe6668d05406db750"
integrity sha512-CG3JnpTZXdmr92GW4zbcba4jkDha6uHraJ7hW4Fn8j0mExxwOKK20hqho8ZuBDCKYCHYIkFM1P2jhtG+KpP4fg==
dependencies:
detect-libc "^1.0.3"
expand-template "^2.0.3"
@ -1162,7 +1165,7 @@ prebuild-install@^5.0.0:
minimist "^1.2.0"
mkdirp "^0.5.1"
napi-build-utils "^1.0.1"
node-abi "^2.2.0"
node-abi "^2.7.0"
noop-logger "^0.1.1"
npmlog "^4.0.1"
os-homedir "^1.0.1"
@ -1183,7 +1186,7 @@ process-nextick-args@~2.0.0:
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==
progress@^2.0.0:
progress@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==

View file

@ -17,6 +17,9 @@
"[Fixed] Embedded Git not working for core.longpath usage in some environments - #7028",
"[Fixed] \"Recover missing repository\" can get stuck in a loop - #7038"
],
"1.6.4-beta0": [
"[Removed] Option to discard when files would be overwritten by a checkout - #7016"
],
"1.6.3": [
"[New] Display \"pull with rebase\" if a user has set this option in their Git config - #6553 #3422",
"[Fixed] Context menu does not open when right clicking on the edges of files in Changes list - #6296. Thanks @JQuinnie!",

View file

@ -157,3 +157,28 @@ file:"C:\ProgramData/Git/config" http.sslcainfo=[some value here]
[http]
sslCAInfo = [some value here]
```
### `ask-pass-trampoline.bat` errors - [#2623](https://github.com/desktop/desktop/issues/2623), [#4124](https://github.com/desktop/desktop/issues/4124), [#6882](https://github.com/desktop/desktop/issues/6882), [#6789](https://github.com/desktop/desktop/issues/6879)
An example of the error message:
```
The system cannot find the path specified.
error: unable to read askpass response from 'C:\Users\User\AppData\Local\GitHubDesktop\app-1.6.2\resources\app\static\ask-pass-trampoline.bat'
fatal: could not read Username for 'https://github.com': terminal prompts disabled"
```
Known causes and workarounds:
- Modifying the `AutoRun` registry entry. To check if this entry has been modified open `Regedit.exe` and navigate to `HKEY_CURRENT_USER\Software\Microsoft\Command Processor\autorun` to see if there is anything set (sometimes applications will also modify this). See [#6789](https://github.com/desktop/desktop/issues/6879#issuecomment-471042891) and [#2623](https://github.com/desktop/desktop/issues/2623#issuecomment-334305916) for examples of this.
- Special characters in your Windows username like a `&` or `-` can cause this error to be thrown. See [#7064](https://github.com/desktop/desktop/issues/7064) for an example of this. Try installing GitHub Desktop in a new user account to verify if this is the case.
- Antivirus software can sometimes prevent GitHub Desktop from installing correctly. If you are running antivirus software that could be causing this try temporarily disabling it and reinstalling GitHub Desktop.
- If none of these potential causes are present on your machine, try performing a fresh installation of GitHub Desktop to see if that gets things working again. Here are the steps you can take to do that:
1. Close GitHub Desktop
2. Delete the `%AppData%\GitHub Desktop\` directory
3. Delete the `%LocalAppData%\GitHubDesktop\` directory
4. Reinstall GitHub Desktop from [desktop.github.com](https://desktop.github.com)

View file

@ -10,13 +10,14 @@ We have a first responder rotation. The goals of the rotation are:
Each rotation is a week long. While first responder your primary duties are:
1. Triage issues.
* Troubleshoot or follow up on troubleshooting started by the previous first responder. The current first responder should always bear responsibility for pushing troubleshooting forward, unless another team member has explicitly taken ownership.
* The current first responder is not responsible for following up on issues still open from previous first responders. However, they should highlight to the team the issues that have been left unanswered for at least 5 days
from previous responders to increase visibility and potentially point out which are highest priority.
* At mention @desktop/support and add `support` label if the issue feels applicable to only the user reporting it, and isn't something more broadly relevant.
* Ensure issues are labeled accurately.
* Review issues labelled [`reviewer-needs-to-reproduce`](https://github.com/desktop/desktop/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+sort%3Aupdated-asc+label%3Areviewer-needs-to-reproduce) and close any that have gone 2 weeks with no new activity after the last question by a reviewer.
* Review issues labelled [`more-information-needed`](https://github.com/desktop/desktop/issues?q=is%3Aopen+is%3Aissue+label%3Amore-information-needed+sort%3Aupdated-asc) and close any that have gone 7 days without an issue template being filled out. Otherwise, the `no-response` bot will close it after 2 weeks.
* Review issues labeled [`reviewer-needs-to-reproduce`](https://github.com/desktop/desktop/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+sort%3Aupdated-asc+label%3Areviewer-needs-to-reproduce) and close any that have gone 2 weeks with no new activity after the last question by a reviewer.
* Review issues labeled [`more-information-needed`](https://github.com/desktop/desktop/issues?q=is%3Aopen+is%3Aissue+label%3Amore-information-needed+sort%3Aupdated-asc) and close any that have gone 7 days without an issue template being filled out. Otherwise, the `no-response` bot will close it after 2 weeks.
* See [issue-triage.md](issue-triage.md) for more information on our issue triage process.
1. Check community pull requests and label ones that are `ready-for-review`.
1. Review community pull requests.
Once those things are done, you should feel free to spend your time scratching your own itches on the project. Really wanna refactor that one monstrous component? Go for it! Wanna fix that one bug that drives you nuts? Do it! Wanna upgrade all of our dependencies? You're an awesome masochist!

View file

@ -84,6 +84,8 @@ issues from time to time that isn't and won't be covered here.
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. Label the issue with `support` if the issue is specific to one person's
configuration and isn't more broadly relevant to other users.
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
@ -104,6 +106,11 @@ Although we use a bot, the first responder should also do a manual sweep of issu
* If a `more-information-needed` issue is stale for more than 14 days after the last comment by a reviewer, the issue will be automatically closed by the no-response bot.
* If the original poster did not fill out the issue template and has not responded to our request within 7 days, close the issue with the following message `I'm closing the issue due to inactivity but I'm happy to re-open if you can provide more details.`
## Support
If an issue reported feels specific to one user's setup and a solution will likely not be relevant to other users of Desktop, the reviewer should add the label `support`
and @-mention @desktop/support so they're able to work with the user to figure out what's causing the problem.
## Needs Reproduction
If a problem is consistently not reproducible, we **need** more information
@ -146,22 +153,6 @@ work should proceed:
e.g. GitHub Desktop should support worktrees as a first class feature.
### Future Proposals
The Desktop team has a [roadmap](roadmap.md) defined for the next few releases,
so that you can see what our future plans look like. Some enhancements suggested
by the community will be for things that are interesting but are also well
beyond the current plans of the team.
We will apply the `future-proposal` label to these issues, so that they can be
searched for when it comes time to plan for the future. However, to keep
our issue tracker focused on tasks currently on the roadmap we will close these
future proposals to avoid information overload.
You can view [the list](https://github.com/desktop/desktop/issues?q=is%3Aissue+label%3Afuture-proposal)
of these `future-proposal` tasks, and continue to add your thoughts and feedback
there.
## Out-of-scope
We anticipate ideas or suggestions that don't align with how we see the

View file

@ -55,7 +55,8 @@
"yarn": ">= 1.9"
},
"dependencies": {
"@typescript-eslint/parser": "^1.0.0",
"@typescript-eslint/eslint-plugin": "1.4.2",
"@typescript-eslint/parser": "1.4.2",
"airbnb-browser-shims": "^3.0.0",
"ajv": "^6.4.0",
"awesome-node-loader": "^1.1.0",
@ -76,7 +77,6 @@
"eslint-plugin-json": "^1.2.1",
"eslint-plugin-prettier": "^3.0.0",
"eslint-plugin-react": "^7.11.1",
"eslint-plugin-typescript": "^0.12.0",
"express": "^4.15.0",
"fake-indexeddb": "^2.0.4",
"file-loader": "^2.0.0",
@ -125,7 +125,7 @@
"@types/clean-webpack-plugin": "^0.1.2",
"@types/codemirror": "0.0.55",
"@types/double-ended-queue": "^2.1.0",
"@types/electron-packager": "^12.0.0",
"@types/electron-packager": "^13.0.0",
"@types/electron-winstaller": "^2.6.0",
"@types/event-kit": "^1.2.28",
"@types/express": "^4.11.0",
@ -166,7 +166,7 @@
"@types/xml2js": "^0.4.0",
"electron": "3.1.6",
"electron-builder": "20.28.4",
"electron-packager": "^12.0.0",
"electron-packager": "^13.1.0",
"electron-winstaller": "2.5.2"
}
}

2
script/globals.d.ts vendored
View file

@ -7,7 +7,7 @@ type Package = {
}
declare namespace NodeJS {
// eslint-disable-next-line typescript/interface-name-prefix
// eslint-disable-next-line @typescript-eslint/interface-name-prefix
interface Process extends EventEmitter {
on(event: 'unhandledRejection', listener: (error: Error) => void): this
}

260
yarn.lock
View file

@ -162,10 +162,10 @@
resolved "https://registry.yarnpkg.com/@types/double-ended-queue/-/double-ended-queue-2.1.0.tgz#adc862d8d53bdf7d1b23a7d85559815ec1fbb92c"
integrity sha512-pCS41/Odn6GMQyqnt8aPTSTQFGriAryYQwVONKk1QhUEhulxueLPE1kDNqDOuJqiv34VLVWXxF4I1EKz3+ftzQ==
"@types/electron-packager@^12.0.0":
version "12.0.0"
resolved "https://registry.yarnpkg.com/@types/electron-packager/-/electron-packager-12.0.0.tgz#acbcf3c5895c1eeda3d2a325c2feeded6a639b1f"
integrity sha512-IocNGjkMkUm/lcUVXCYw0mIqXkoce1NEZ4oV4Td4LwHKXZqWzxdKlPjwnP52LNX6ghddjjXUPukPeKHMHMpxEA==
"@types/electron-packager@^13.0.0":
version "13.0.0"
resolved "https://registry.yarnpkg.com/@types/electron-packager/-/electron-packager-13.0.0.tgz#d391366bc9722587f4c5e254f4071a2c71fc960c"
integrity sha512-Q0e/ja/TfSSPM5rV9RxaPz8yew7dWSSufFsziw08hCZYQuafXkbhxs83UvQ/Hte3+c3U+ogLQKXFasGn1umn5A==
dependencies:
"@types/node" "*"
@ -317,9 +317,9 @@
integrity sha512-kOwL746WVvt/9Phf6/JgX/bsGQvbrK5iUgzyfwZNcKVFcjAUVSpF9HxevLTld2SG9aywYHOILj38arDdY1r/iQ==
"@types/node@^8.0.24":
version "8.10.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.1.tgz#aac98b810c50568054486f2bb8c486d824713be8"
integrity sha512-X/pIUOcgpX7xxKsmdPCMKeDBefsGH/4D/IuJ1gIHbqgWI0qEy/yMKeqaN/sT+rzV9UpAXAfd0kLOVExRmZrXIg==
version "8.10.40"
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.40.tgz#4314888d5cd537945d73e9ce165c04cc550144a4"
integrity sha512-RRSjdwz63kS4u7edIwJUn8NqKLLQ6LyqF/X4+4jp38MBT3Vwetewi2N4dgJEshLbDwNgOJXNYoOwzVZUSSLhkQ==
"@types/node@^8.10.4":
version "8.10.4"
@ -529,19 +529,29 @@
dependencies:
"@types/node" "*"
"@typescript-eslint/parser@^1.0.0":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-1.1.1.tgz#a979c5dc543ae4ae9b44df9def70e2a5892e7185"
integrity sha512-P6v+iYkI+ywp6MaFyAJ6NqU5W6fiAvMXWjCV63xTJbkQdtAngdjSCajlEEweqJqL4RNsgFCHBe5HbYyT6TmW4g==
"@typescript-eslint/eslint-plugin@1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.4.2.tgz#370bc32022d1cc884a5dcf62624ef2024182769d"
integrity sha512-6WInypy/cK4rM1dirKbD5p7iFW28DbSRKT/+PGn+DYzBWEvHq5KnZAqQ5cX25JBc0qMkFxJNxNfBbFXJyyzVcw==
dependencies:
"@typescript-eslint/typescript-estree" "1.1.1"
"@typescript-eslint/parser" "1.4.2"
"@typescript-eslint/typescript-estree" "1.4.2"
requireindex "^1.2.0"
tsutils "^3.7.0"
"@typescript-eslint/parser@1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-1.4.2.tgz#acfdee2019958a41d308d768e53ded975ef90ce8"
integrity sha512-OqLkY9295DXXaWToItUv3olO2//rmzh6Th6Sc7YjFFEpEuennsm5zhygLLvHZjPxPlzrQgE8UDaOPurDylaUuw==
dependencies:
"@typescript-eslint/typescript-estree" "1.4.2"
eslint-scope "^4.0.0"
eslint-visitor-keys "^1.0.0"
"@typescript-eslint/typescript-estree@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-1.1.1.tgz#d5cccc227d2c8948799d127b6cc51ccb5378bf51"
integrity sha512-rERZSjNWb4WC425daCUktfh+0fFLy4WWlnu9bESdJv5l+t0ww0yUprRUbgzehag/dGd56Me+3uyXGV2O12qxrQ==
"@typescript-eslint/typescript-estree@1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-1.4.2.tgz#b16bc36c9a4748a7fca92cba4c2d73c5325c8a85"
integrity sha512-wKgi/w6k1v3R4b6oDc20cRWro2gBzp0wn6CAeYC8ExJMfvXMfiaXzw2tT9ilxdONaVWMCk7B9fMdjos7bF/CWw==
dependencies:
lodash.unescape "4.0.1"
semver "5.5.0"
@ -1134,19 +1144,19 @@ asar@^0.11.0:
mkdirp "^0.5.0"
mksnapshot "^0.3.0"
asar@^0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/asar/-/asar-0.14.0.tgz#998b36a26abd0e590e55d9f92cfd3fd7a6051652"
integrity sha512-l21mf5pG65qbtD5WhymthfbE7ash0goQ+5ayo3lIncxtFNYH1PVArqsGXoAUXOd877mJplWSD9nGumByzQqVSA==
asar@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/asar/-/asar-1.0.0.tgz#5624ffa1369aa929871dfc036de02c20871bdc2e"
integrity sha512-MBiDU5cDr9UWuY2F0zq2fZlnyRq1aOPmJGMas22Qa14K1odpRXL3xkMHPN3uw2hAK5mD89Q+/KidOUtpi4V0Cg==
dependencies:
chromium-pickle-js "^0.2.0"
commander "^2.9.0"
cuint "^0.2.1"
glob "^6.0.4"
minimatch "^3.0.3"
mkdirp "^0.5.0"
mksnapshot "^0.3.0"
tmp "0.0.28"
commander "^2.19.0"
cuint "^0.2.2"
glob "^7.1.3"
minimatch "^3.0.4"
mkdirp "^0.5.1"
pify "^4.0.1"
tmp-promise "^1.0.5"
asn1.js@^4.0.0:
version "4.9.2"
@ -2198,6 +2208,11 @@ camelcase@^4.0.0, camelcase@^4.1.0:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
camelcase@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42"
integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==
capture-exit@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-1.2.0.tgz#1c5fcc489fd0ab00d4f1ac7ae1072e3173fbab6f"
@ -2557,6 +2572,11 @@ commander@^2.12.1, commander@^2.13.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==
commander@^2.19.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==
commander@~2.13.0:
version "2.13.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
@ -2915,7 +2935,7 @@ csstype@^2.2.0:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.3.0.tgz#062e141c78345cf814da0e0b716ad777931b08af"
integrity sha512-+iowf+HbYUKV65+HjAhXkx4KH6IFpIxnBlO0maKsXmBIHJXEndaTRYPVL4pEwtK6+1zRvkXo+WD1tRFKygMHQg==
cuint@^0.2.1:
cuint@^0.2.1, cuint@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b"
integrity sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs=
@ -2991,12 +3011,19 @@ debug@^4.0.1:
dependencies:
ms "^2.1.1"
debug@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
dependencies:
ms "^2.1.1"
debuglog@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=
decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2:
decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
@ -3351,7 +3378,7 @@ electron-chromedriver@~3.0.0:
electron-download "^4.1.0"
extract-zip "^1.6.5"
electron-download@^4.0.0, electron-download@^4.1.0:
electron-download@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/electron-download/-/electron-download-4.1.0.tgz#bf932c746f2f87ffcc09d1dd472f2ff6b9187845"
integrity sha1-v5MsdG8vh//MCdHdRy8v9rkYeEU=
@ -3366,6 +3393,29 @@ electron-download@^4.0.0, electron-download@^4.1.0:
semver "^5.3.0"
sumchecker "^2.0.1"
electron-download@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/electron-download/-/electron-download-4.1.1.tgz#02e69556705cc456e520f9e035556ed5a015ebe8"
integrity sha512-FjEWG9Jb/ppK/2zToP+U5dds114fM1ZOJqMAR4aXXL5CvyPE9fiqBK/9YcwC9poIFQTEJk/EM/zyRwziziRZrg==
dependencies:
debug "^3.0.0"
env-paths "^1.0.0"
fs-extra "^4.0.1"
minimist "^1.2.0"
nugget "^2.0.1"
path-exists "^3.0.0"
rc "^1.2.1"
semver "^5.4.1"
sumchecker "^2.0.2"
electron-notarize@^0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/electron-notarize/-/electron-notarize-0.0.5.tgz#d9e95c763a6af853ce16d31dde72d73cb25b0703"
integrity sha512-YzrqZ6RDQ7Wt2RWlxzRoQUuxnTeXrfp7laH7XKcmQqrZ6GaAr50DMPvFMpqDKdrZSHSbcgZgB7ktIQbjvITmCQ==
dependencies:
debug "^4.1.0"
fs-extra "^7.0.0"
electron-osx-sign@0.4.10:
version "0.4.10"
resolved "https://registry.yarnpkg.com/electron-osx-sign/-/electron-osx-sign-0.4.10.tgz#be4f3b89b2a75a1dc5f1e7249081ab2929ca3a26"
@ -3378,40 +3428,40 @@ electron-osx-sign@0.4.10:
minimist "^1.2.0"
plist "^2.1.0"
electron-osx-sign@^0.4.1:
version "0.4.7"
resolved "https://registry.yarnpkg.com/electron-osx-sign/-/electron-osx-sign-0.4.7.tgz#1d75647a82748eacd48bea70616ec83ffade3ee5"
integrity sha1-HXVkeoJ0jqzUi+pwYW7IP/rePuU=
electron-osx-sign@^0.4.11:
version "0.4.11"
resolved "https://registry.yarnpkg.com/electron-osx-sign/-/electron-osx-sign-0.4.11.tgz#8377732fe7b207969f264b67582ee47029ce092f"
integrity sha512-VVd40nrnVqymvFrY9ZkOYgHJOvexHHYTR3di/SN+mjJ0OWhR1I8BRVj3U+Yamw6hnkZZNKZp52rqL5EFAAPFkQ==
dependencies:
bluebird "^3.5.0"
compare-version "^0.1.2"
debug "^2.6.8"
isbinaryfile "^3.0.2"
minimist "^1.2.0"
plist "^2.1.0"
plist "^3.0.1"
electron-packager@^12.0.0:
version "12.1.0"
resolved "https://registry.yarnpkg.com/electron-packager/-/electron-packager-12.1.0.tgz#048dd4ff3848be19c5873c315b5b312df6215328"
integrity sha1-BI3U/zhIvhnFhzwxW1sxLfYhUyg=
electron-packager@^13.1.0:
version "13.1.0"
resolved "https://registry.yarnpkg.com/electron-packager/-/electron-packager-13.1.0.tgz#be2d2ecb5ca08932d8e2ec20d02925c245e3780d"
integrity sha512-XHoDqgG90NGBfgUJ3NcOmELAuvHucOIYzi7AhZKIC8FivRR45PDs0pXSf53OqTXXOkdL/1xSveogvJLjKJwkAQ==
dependencies:
asar "^0.14.0"
debug "^3.0.0"
electron-download "^4.0.0"
electron-osx-sign "^0.4.1"
asar "^1.0.0"
debug "^4.0.1"
electron-download "^4.1.1"
electron-notarize "^0.0.5"
electron-osx-sign "^0.4.11"
extract-zip "^1.0.3"
fs-extra "^5.0.0"
fs-extra "^7.0.0"
galactus "^0.2.1"
get-package-info "^1.0.0"
nodeify "^1.0.1"
parse-author "^2.0.0"
pify "^3.0.0"
plist "^2.0.0"
pify "^4.0.0"
plist "^3.0.0"
rcedit "^1.0.0"
resolve "^1.1.6"
sanitize-filename "^1.6.0"
semver "^5.3.0"
yargs-parser "^10.0.0"
yargs-parser "^13.0.0"
electron-publish@20.28.3:
version "20.28.3"
@ -3646,13 +3696,6 @@ eslint-plugin-react@^7.11.1:
jsx-ast-utils "^2.0.1"
prop-types "^15.6.2"
eslint-plugin-typescript@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-typescript/-/eslint-plugin-typescript-0.12.0.tgz#e23d58cb27fe28e89fc641a1f20e8d862cb99aef"
integrity sha512-2+DNE8nTvdNkhem/FBJXLPSeMDOZL68vHHNfTbM+PBc5iAuwBe8xLSQubwKxABqSZDwUHg+mwGmv5c2NlImi0Q==
dependencies:
requireindex "~1.1.0"
eslint-rule-composer@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9"
@ -4353,7 +4396,7 @@ fs-extra@^2.0.0:
graceful-fs "^4.1.2"
jsonfile "^2.1.0"
fs-extra@^4.0.0:
fs-extra@^4.0.0, fs-extra@^4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94"
integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==
@ -4362,15 +4405,6 @@ fs-extra@^4.0.0:
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-extra@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-5.0.0.tgz#414d0110cdd06705734d055652c5411260c31abd"
integrity sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==
dependencies:
graceful-fs "^4.1.2"
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-extra@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-6.0.0.tgz#0f0afb290bb3deb87978da816fcd3c7797f3a817"
@ -4380,6 +4414,15 @@ fs-extra@^6.0.0:
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-extra@^7.0.0:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==
dependencies:
graceful-fs "^4.1.2"
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-minipass@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d"
@ -4599,6 +4642,18 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^7.1.3:
version "7.1.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
global-dirs@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.0.tgz#10d34039e0df04272e262cf24224f7209434df4f"
@ -5416,11 +5471,6 @@ is-promise@^2.1.0:
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=
is-promise@~1, is-promise@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-1.0.1.tgz#31573761c057e33c2e91aab9e96da08cefbe76e5"
integrity sha1-MVc3YcBX4zwukaq56W2gjO++duU=
is-redirect@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24"
@ -7189,14 +7239,6 @@ node-sass@^4.11.0:
stdout-stream "^1.4.0"
"true-case-path" "^1.0.2"
nodeify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/nodeify/-/nodeify-1.0.1.tgz#64ab69a7bdbaf03ce107b4f0335c87c0b9e91b1d"
integrity sha1-ZKtpp7268DzhB7TwM1yHwLnpGx0=
dependencies:
is-promise "~1.0.0"
promise "~1.3.0"
"nopt@2 || 3", nopt@^3.0.1:
version "3.0.6"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
@ -7278,7 +7320,7 @@ nth-check@~1.0.1:
dependencies:
boolbase "~1.0.0"
nugget@^2.0.0:
nugget@^2.0.0, nugget@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/nugget/-/nugget-2.0.1.tgz#201095a487e1ad36081b3432fa3cada4f8d071b0"
integrity sha1-IBCVpIfhrTYIGzQy+jytpPjQcbA=
@ -7474,7 +7516,7 @@ os-locale@^2.0.0:
lcid "^1.0.0"
mem "^1.1.0"
os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2:
os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
@ -7747,6 +7789,11 @@ pify@^3.0.0:
resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
pify@^4.0.0, pify@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
pinkie-promise@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
@ -7766,7 +7813,7 @@ pkg-dir@^2.0.0:
dependencies:
find-up "^2.1.0"
plist@^2.0.0, plist@^2.1.0:
plist@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/plist/-/plist-2.1.0.tgz#57ccdb7a0821df21831217a3cad54e3e146a1025"
integrity sha1-V8zbeggh3yGDEhejytVOPhRqECU=
@ -7775,7 +7822,7 @@ plist@^2.0.0, plist@^2.1.0:
xmlbuilder "8.2.2"
xmldom "0.1.x"
plist@^3.0.1:
plist@^3.0.0, plist@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/plist/-/plist-3.0.1.tgz#a9b931d17c304e8912ef0ba3bdd6182baf2e1f8c"
integrity sha512-GpgvHHocGRyQm74b6FWEZZVRroHKE1I0/BTjAmySaohK+cUn+hZpbqXkc3KWgW3gQYkqcQej35FohcT0FRlkRQ==
@ -7976,13 +8023,6 @@ promise.prototype.finally@^3.1.0:
es-abstract "^1.9.0"
function-bind "^1.1.1"
promise@~1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/promise/-/promise-1.3.0.tgz#e5cc9a4c8278e4664ffedc01c7da84842b040175"
integrity sha1-5cyaTIJ45GZP/twBx9qEhCsEAXU=
dependencies:
is-promise "~1"
prompts@^0.1.9:
version "0.1.14"
resolved "https://registry.yarnpkg.com/prompts/-/prompts-0.1.14.tgz#a8e15c612c5c9ec8f8111847df3337c9cbd443b2"
@ -8175,7 +8215,7 @@ rc@^1.0.1, rc@^1.1.2, rc@^1.1.6, rc@^1.1.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
rc@^1.2.7:
rc@^1.2.1, rc@^1.2.7:
version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
@ -8579,10 +8619,10 @@ require-uncached@^1.0.3:
caller-path "^0.1.0"
resolve-from "^1.0.0"
requireindex@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.1.0.tgz#e5404b81557ef75db6e49c5a72004893fe03e162"
integrity sha1-5UBLgVV+91225JxacgBIk/4D4WI=
requireindex@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef"
integrity sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==
resolve-cwd@^2.0.0:
version "2.0.0"
@ -9457,7 +9497,7 @@ style-loader@^0.21.0:
loader-utils "^1.1.0"
schema-utils "^0.4.5"
sumchecker@^2.0.1:
sumchecker@^2.0.1, sumchecker@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-2.0.2.tgz#0f42c10e5d05da5d42eea3e56c3399a37d6c5b3e"
integrity sha1-D0LBDl0F2l1C7qPlbDOZo31sWz4=
@ -9652,14 +9692,15 @@ timers-browserify@^2.0.2:
dependencies:
setimmediate "^1.0.4"
tmp@0.0.28:
version "0.0.28"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.28.tgz#172735b7f614ea7af39664fa84cf0de4e515d120"
integrity sha1-Fyc1t/YU6nrzlmT6hM8N5OUV0SA=
tmp-promise@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-1.0.5.tgz#3208d7fa44758f86a2a4c4060f3c33fea30e8038"
integrity sha512-hOabTz9Tp49wCozFwuJe5ISrOqkECm6kzw66XTP23DuzNU7QS/KiZq5LC9Y7QSy8f1rPSLy4bKaViP0OwGI1cA==
dependencies:
os-tmpdir "~1.0.1"
bluebird "^3.5.0"
tmp "0.0.33"
tmp@^0.0.33:
tmp@0.0.33, tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
@ -9899,6 +9940,13 @@ tsutils@^2.27.2:
dependencies:
tslib "^1.8.1"
tsutils@^3.7.0:
version "3.8.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.8.0.tgz#7a3dbadc88e465596440622b65c04edc8e187ae5"
integrity sha512-XQdPhgcoTbCD8baXC38PQ0vpTZ8T3YrE+vR66YIj/xvDt1//8iAhafpIT/4DmvzzC1QFapEImERu48Pa01dIUA==
dependencies:
tslib "^1.8.1"
tty-browserify@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
@ -10783,13 +10831,21 @@ yallist@^3.0.0, yallist@^3.0.2:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9"
integrity sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=
yargs-parser@^10.0.0, yargs-parser@^10.1.0:
yargs-parser@^10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8"
integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==
dependencies:
camelcase "^4.1.0"
yargs-parser@^13.0.0:
version "13.0.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.0.0.tgz#3fc44f3e76a8bdb1cc3602e860108602e5ccde8b"
integrity sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==
dependencies:
camelcase "^5.0.0"
decamelize "^1.2.0"
yargs-parser@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a"