mirror of
https://github.com/desktop/desktop
synced 2024-11-05 20:49:32 +00:00
Merge branch 'development' into releases/1.6.5
This commit is contained in:
commit
9f1167bace
84 changed files with 2097 additions and 913 deletions
|
@ -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 #
|
||||
|
|
|
@ -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",
|
||||
|
|
2
app/src/highlighter/globals.d.ts
vendored
2
app/src/highlighter/globals.d.ts
vendored
|
@ -1,4 +1,4 @@
|
|||
/* eslint-disable typescript/interface-name-prefix */
|
||||
/* eslint-disable @typescript-eslint/interface-name-prefix */
|
||||
|
||||
declare namespace CodeMirror {
|
||||
interface EditorConfiguration {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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}`)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
84
app/src/lib/git/stash.ts
Normal 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}>`
|
||||
}
|
|
@ -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.
|
||||
|
|
2
app/src/lib/globals.d.ts
vendored
2
app/src/lib/globals.d.ts
vendored
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 => ({
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
||||
const apiCommit = await api.fetchCommit(
|
||||
repository.owner.login,
|
||||
repository.name,
|
||||
sha
|
||||
)
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -127,6 +127,7 @@ function getInitialRepositoryState(): IRepositoryState {
|
|||
openPullRequests: new Array<PullRequest>(),
|
||||
currentPullRequest: null,
|
||||
isLoadingPullRequests: false,
|
||||
rebasedBranches: new Map<string, string>(),
|
||||
},
|
||||
compareState: {
|
||||
isDivergingBranchBannerVisible: false,
|
||||
|
|
|
@ -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',
|
||||
})
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -4,6 +4,7 @@ export type MenuIDs =
|
|||
| 'preferences'
|
||||
| 'update-branch'
|
||||
| 'merge-branch'
|
||||
| 'rebase-branch'
|
||||
| 'view-repository-on-github'
|
||||
| 'compare-on-github'
|
||||
| 'open-in-shell'
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ export function SuccessfulRebase({
|
|||
<span>
|
||||
{'Successfully rebased '}
|
||||
<strong>{targetBranch}</strong>
|
||||
{' on '}
|
||||
{' onto '}
|
||||
<strong>{baseBranch}</strong>
|
||||
</span>
|
||||
) : (
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -24,24 +24,33 @@ const prLoadingItemProps: IPullRequestListItemProps = {
|
|||
},
|
||||
}
|
||||
|
||||
const items: Array<IFilterListItem> = []
|
||||
|
||||
for (let i = 0; i < FacadeCount; i++) {
|
||||
items.push({
|
||||
text: [''],
|
||||
id: i.toString(),
|
||||
})
|
||||
}
|
||||
|
||||
const groups = [
|
||||
{
|
||||
identifier: '',
|
||||
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.Component<{}, {}> {
|
||||
export class PullRequestsLoading extends React.PureComponent<
|
||||
IPullRequestLoadingProps,
|
||||
{}
|
||||
> {
|
||||
public render() {
|
||||
const items: Array<IFilterListItem> = []
|
||||
for (let i = 0; i < FacadeCount; i++) {
|
||||
items.push({
|
||||
text: [''],
|
||||
id: i.toString(),
|
||||
})
|
||||
}
|
||||
|
||||
const groups = [
|
||||
{
|
||||
identifier: '',
|
||||
items,
|
||||
},
|
||||
]
|
||||
|
||||
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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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}`),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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')}
|
||||
|
|
|
@ -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)
|
||||
) {
|
||||
this.setBanner({
|
||||
type: BannerType.SuccessfulRebase,
|
||||
targetBranch: conflictState.targetBranch,
|
||||
})
|
||||
}
|
||||
if (result === RebaseResult.CompletedWithoutError) {
|
||||
this.closePopup()
|
||||
|
||||
return result
|
||||
if (conflictState !== null && isRebaseConflictState(conflictState)) {
|
||||
this.setBanner({
|
||||
type: BannerType.SuccessfulRebase,
|
||||
targetBranch: conflictState.targetBranch,
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
2
app/src/ui/lib/conflicts/index.ts
Normal file
2
app/src/ui/lib/conflicts/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './render-functions'
|
||||
export * from './unmerged-file'
|
34
app/src/ui/lib/conflicts/render-functions.tsx
Normal file
34
app/src/ui/lib/conflicts/render-functions.tsx
Normal 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>
|
||||
)
|
||||
}
|
|
@ -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) {
|
|
@ -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)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
|
|
89
app/src/ui/rebase/confirm-force-push.tsx
Normal file
89
app/src/ui/rebase/confirm-force-push.tsx
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
198
app/src/ui/rebase/rebase-branch-dialog.tsx
Normal file
198
app/src/ui/rebase/rebase-branch-dialog.tsx
Normal 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
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
@import '../../mixins';
|
||||
|
||||
dialog#merge-conflicts-list {
|
||||
dialog#merge-conflicts-list,
|
||||
dialog#rebase-conflicts-list {
|
||||
width: 500px;
|
||||
|
||||
.summary {
|
||||
|
|
115
app/styles/ui/dialogs/_rebase-conflicts.scss
Normal file
115
app/styles/ui/dialogs/_rebase-conflicts.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
font-weight: var(--font-weight-light);
|
||||
// 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-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;
|
||||
}
|
||||
.filter-field-row {
|
||||
margin: 0;
|
||||
border-bottom: var(--base-border);
|
||||
|
||||
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;
|
||||
.filter-list-filter-field {
|
||||
padding: 0 var(--spacing-double);
|
||||
padding-bottom: var(--spacing);
|
||||
}
|
||||
}
|
||||
|
||||
.unmerged-file-status-resolved .file-conflicts-status {
|
||||
color: $green;
|
||||
}
|
||||
.list-item {
|
||||
padding: 0 var(--spacing-double);
|
||||
|
||||
.unmerged-file-status-conflicts {
|
||||
.file-conflicts-status {
|
||||
color: $orange;
|
||||
}
|
||||
|
||||
.command-line-hint {
|
||||
color: $gray;
|
||||
.filter-list-group-header,
|
||||
.branches-list-item {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
11
app/test/helpers/local-config.ts
Normal file
11
app/test/helpers/local-config.ts
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
|
|
@ -210,8 +210,6 @@ async function initializeTestRepo(
|
|||
url: '',
|
||||
login: '',
|
||||
avatar_url: '',
|
||||
name: null,
|
||||
email: null,
|
||||
type: 'User',
|
||||
},
|
||||
private: false,
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
// ensure this repository only ever sticks to LF
|
||||
await GitProcess.exec(['config', '--local', 'core.eol', 'lf'], repo.path)
|
||||
setupLocalConfig(repo, [
|
||||
// ensure this repository only ever sticks to LF
|
||||
['core.eol', 'lf'],
|
||||
// do not do any conversion of line endings when committing
|
||||
['core.autocrlf', 'input'],
|
||||
])
|
||||
|
||||
// do not do any conversion of line endings when committing
|
||||
await GitProcess.exec(
|
||||
['config', '--local', 'core.autocrlf', 'input'],
|
||||
repo.path
|
||||
)
|
||||
|
||||
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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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`, () => {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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'
|
||||
|
|
73
app/test/unit/git/stash-test.ts
Normal file
73
app/test/unit/git/stash-test.ts
Normal 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)
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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==
|
||||
|
|
|
@ -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!",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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!
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
2
script/globals.d.ts
vendored
|
@ -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
260
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue