mirror of
https://github.com/desktop/desktop
synced 2024-09-17 23:21:55 +00:00
Merge branch 'master' into upgrade-eslint-dependencies
This commit is contained in:
commit
8dabdbeeb2
|
@ -24,12 +24,3 @@ coverage:
|
|||
changes: no
|
||||
|
||||
comment: off
|
||||
|
||||
# not sure what this does, but its the default!
|
||||
parsers:
|
||||
gcov:
|
||||
branch_detection:
|
||||
conditional: yes
|
||||
loop: yes
|
||||
method: no
|
||||
macro: no
|
||||
|
|
|
@ -1 +1 @@
|
|||
8.11.4
|
||||
8.12.0
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
python 2.7
|
||||
nodejs 8.11.4
|
||||
nodejs 8.12.0
|
||||
|
|
|
@ -38,7 +38,7 @@ branches:
|
|||
|
||||
language: node_js
|
||||
node_js:
|
||||
- '8.11.4'
|
||||
- '8.12'
|
||||
|
||||
cache:
|
||||
yarn: true
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"productName": "GitHub Desktop",
|
||||
"bundleID": "com.github.GitHubClient",
|
||||
"companyName": "GitHub, Inc.",
|
||||
"version": "1.5.0-beta3",
|
||||
"version": "1.5.1-beta0",
|
||||
"main": "./main.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -5,7 +5,7 @@ import { CrashApp } from './crash-app'
|
|||
|
||||
if (!process.env.TEST_ENV) {
|
||||
/* This is the magic trigger for webpack to go compile
|
||||
* our sass into css and inject it into the DOM. */
|
||||
* our sass into css and inject it into the DOM. */
|
||||
require('./styles/crash.scss')
|
||||
}
|
||||
|
||||
|
|
|
@ -27,22 +27,23 @@ interface IModeDefinition {
|
|||
|
||||
/**
|
||||
* A map between file extensions (including the leading dot, i.e.
|
||||
* ".jpeg") and the selected mime type to use when highlighting
|
||||
* that extension as specified in the CodeMirror mode itself.
|
||||
* ".jpeg") or basenames (i.e. "dockerfile") and the selected mime
|
||||
* type to use when highlighting that extension as specified in
|
||||
* the CodeMirror mode itself.
|
||||
*/
|
||||
readonly extensions: {
|
||||
readonly mappings: {
|
||||
readonly [key: string]: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Array describing all currently supported modes and the file extensions
|
||||
* Array describing all currently supported extensionModes and the file extensions
|
||||
* that they cover.
|
||||
*/
|
||||
const modes: ReadonlyArray<IModeDefinition> = [
|
||||
const extensionModes: ReadonlyArray<IModeDefinition> = [
|
||||
{
|
||||
install: () => import('codemirror/mode/javascript/javascript'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.ts': 'text/typescript',
|
||||
'.js': 'text/javascript',
|
||||
'.json': 'application/json',
|
||||
|
@ -50,33 +51,33 @@ const modes: ReadonlyArray<IModeDefinition> = [
|
|||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/coffeescript/coffeescript'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.coffee': 'text/x-coffeescript',
|
||||
},
|
||||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/jsx/jsx'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.tsx': 'text/typescript-jsx',
|
||||
'.jsx': 'text/jsx',
|
||||
},
|
||||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/htmlmixed/htmlmixed'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.html': 'text/html',
|
||||
'.htm': 'text/html',
|
||||
},
|
||||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/htmlembedded/htmlembedded'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.jsp': 'application/x-jsp',
|
||||
},
|
||||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/css/css'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.css': 'text/css',
|
||||
'.scss': 'text/x-scss',
|
||||
'.less': 'text/x-less',
|
||||
|
@ -84,27 +85,27 @@ const modes: ReadonlyArray<IModeDefinition> = [
|
|||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/vue/vue'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.vue': 'text/x-vue',
|
||||
},
|
||||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/markdown/markdown'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.markdown': 'text/x-markdown',
|
||||
'.md': 'text/x-markdown',
|
||||
},
|
||||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/yaml/yaml'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.yaml': 'text/yaml',
|
||||
'.yml': 'text/yaml',
|
||||
},
|
||||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/xml/xml'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.xml': 'text/xml',
|
||||
'.xaml': 'text/xml',
|
||||
'.csproj': 'text/xml',
|
||||
|
@ -116,7 +117,7 @@ const modes: ReadonlyArray<IModeDefinition> = [
|
|||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/clike/clike'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.m': 'text/x-objectivec',
|
||||
'.scala': 'text/x-scala',
|
||||
'.sc': 'text/x-scala',
|
||||
|
@ -132,7 +133,7 @@ const modes: ReadonlyArray<IModeDefinition> = [
|
|||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/mllike/mllike'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.ml': 'text/x-ocaml',
|
||||
'.fs': 'text/x-fsharp',
|
||||
'.fsx': 'text/x-fsharp',
|
||||
|
@ -141,61 +142,61 @@ const modes: ReadonlyArray<IModeDefinition> = [
|
|||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/swift/swift'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.swift': 'text/x-swift',
|
||||
},
|
||||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/shell/shell'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.sh': 'text/x-sh',
|
||||
},
|
||||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/sql/sql'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.sql': 'text/x-sql',
|
||||
},
|
||||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/cypher/cypher'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.cql': 'application/x-cypher-query',
|
||||
},
|
||||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/go/go'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.go': 'text/x-go',
|
||||
},
|
||||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/perl/perl'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.pl': 'text/x-perl',
|
||||
},
|
||||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/php/php'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.php': 'application/x-httpd-php',
|
||||
},
|
||||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/python/python'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.py': 'text/x-python',
|
||||
},
|
||||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/ruby/ruby'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.rb': 'text/x-ruby',
|
||||
},
|
||||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/clojure/clojure'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.clj': 'text/x-clojure',
|
||||
'.cljc': 'text/x-clojure',
|
||||
'.cljs': 'text/x-clojure',
|
||||
|
@ -204,32 +205,32 @@ const modes: ReadonlyArray<IModeDefinition> = [
|
|||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/rust/rust'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.rs': 'text/x-rustsrc',
|
||||
},
|
||||
},
|
||||
{
|
||||
install: () => import('codemirror-mode-elixir'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.ex': 'text/x-elixir',
|
||||
'.exs': 'text/x-elixir',
|
||||
},
|
||||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/haxe/haxe'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.hx': 'text/x-haxe',
|
||||
},
|
||||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/r/r'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.r': 'text/x-rsrc',
|
||||
},
|
||||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/powershell/powershell'),
|
||||
extensions: {
|
||||
mappings: {
|
||||
'.ps1': 'application/x-powershell',
|
||||
},
|
||||
},
|
||||
|
@ -237,11 +238,31 @@ const modes: ReadonlyArray<IModeDefinition> = [
|
|||
|
||||
/**
|
||||
* A map between file extensions and mime types, see
|
||||
* the 'extensions' property on the IModeDefinition interface
|
||||
* the 'mappings' property on the IModeDefinition interface
|
||||
* for more information
|
||||
*/
|
||||
const extensionMIMEMap = new Map<string, string>()
|
||||
|
||||
/**
|
||||
* Array describing all currently supported basenameModes and the file names
|
||||
* that they cover.
|
||||
*/
|
||||
const basenameModes: ReadonlyArray<IModeDefinition> = [
|
||||
{
|
||||
install: () => import('codemirror/mode/dockerfile/dockerfile'),
|
||||
mappings: {
|
||||
dockerfile: 'text/x-dockerfile',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* A map between file basenames and mime types, see
|
||||
* the 'basenames' property on the IModeDefinition interface
|
||||
* for more information
|
||||
*/
|
||||
const basenameMIMEMap = new Map<string, string>()
|
||||
|
||||
/**
|
||||
* A map between mime types and mode definitions. See the
|
||||
* documentation for the IModeDefinition interface
|
||||
|
@ -249,10 +270,17 @@ const extensionMIMEMap = new Map<string, string>()
|
|||
*/
|
||||
const mimeModeMap = new Map<string, IModeDefinition>()
|
||||
|
||||
for (const mode of modes) {
|
||||
for (const [extension, mimeType] of Object.entries(mode.extensions)) {
|
||||
extensionMIMEMap.set(extension, mimeType)
|
||||
mimeModeMap.set(mimeType, mode)
|
||||
for (const extensionMode of extensionModes) {
|
||||
for (const [mapping, mimeType] of Object.entries(extensionMode.mappings)) {
|
||||
extensionMIMEMap.set(mapping, mimeType)
|
||||
mimeModeMap.set(mimeType, extensionMode)
|
||||
}
|
||||
}
|
||||
|
||||
for (const basenameMode of basenameModes) {
|
||||
for (const [mapping, mimeType] of Object.entries(basenameMode.mappings)) {
|
||||
basenameMIMEMap.set(mapping, mimeType)
|
||||
mimeModeMap.set(mimeType, basenameMode)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -293,6 +321,7 @@ async function detectMode(
|
|||
): Promise<CodeMirror.Mode<{}> | null> {
|
||||
const mimeType =
|
||||
extensionMIMEMap.get(request.extension.toLowerCase()) ||
|
||||
basenameMIMEMap.get(request.basename.toLowerCase()) ||
|
||||
guessMimeType(request.contents)
|
||||
|
||||
if (!mimeType) {
|
||||
|
|
|
@ -163,6 +163,16 @@ export interface IAppState {
|
|||
/** The external editor to use when opening repositories */
|
||||
readonly selectedExternalEditor?: ExternalEditor
|
||||
|
||||
/**
|
||||
* A cached entry representing an external editor found on the user's machine:
|
||||
*
|
||||
* - If the `selectedExternalEditor` can be found, choose that
|
||||
* - Otherwise, if any editors found, this will be set to the first value
|
||||
* based on the search order in `app/src/lib/editors/{platform}.ts`
|
||||
* - If no editors found, this will remain `null`
|
||||
*/
|
||||
readonly resolvedExternalEditor: ExternalEditor | null
|
||||
|
||||
/** What type of visual diff mode we should use to compare images */
|
||||
readonly imageDiffType: ImageDiffType
|
||||
|
||||
|
@ -223,8 +233,9 @@ export enum RepositorySectionTab {
|
|||
/**
|
||||
* Stores information about a merge conflict when it occurs
|
||||
*/
|
||||
interface IConflictState {
|
||||
readonly branch: Branch
|
||||
export interface IConflictState {
|
||||
readonly currentBranch: string
|
||||
readonly currentTip: string
|
||||
}
|
||||
|
||||
export interface IRepositoryState {
|
||||
|
@ -525,6 +536,6 @@ export interface ICompareToBranch {
|
|||
export type CompareAction = IViewHistory | ICompareToBranch
|
||||
|
||||
export type SuccessfulMergeBannerState = {
|
||||
currentBranch: string
|
||||
theirBranch: string
|
||||
ourBranch: string
|
||||
theirBranch?: string
|
||||
} | null
|
||||
|
|
|
@ -22,7 +22,7 @@ import {
|
|||
import { AppStore } from '../stores/app-store'
|
||||
import { CloningRepository } from '../../models/cloning-repository'
|
||||
import { Branch } from '../../models/branch'
|
||||
import { Commit } from '../../models/commit'
|
||||
import { Commit, ICommitContext } from '../../models/commit'
|
||||
import { ExternalEditor } from '../../lib/editors'
|
||||
import { IAPIUser } from '../../lib/api'
|
||||
import { GitHubRepository } from '../../models/github-repository'
|
||||
|
@ -57,7 +57,6 @@ import { BranchesTab } from '../../models/branches-tab'
|
|||
import { FetchType } from '../../models/fetch'
|
||||
import { PullRequest } from '../../models/pull-request'
|
||||
import { IAuthor } from '../../models/author'
|
||||
import { ITrailer } from '../git/interpret-trailers'
|
||||
import { isGitRepository } from '../git'
|
||||
import { ApplicationTheme } from '../../ui/lib/application-theme'
|
||||
import { TipState } from '../../models/tip'
|
||||
|
@ -208,16 +207,9 @@ export class Dispatcher {
|
|||
*/
|
||||
public async commitIncludedChanges(
|
||||
repository: Repository,
|
||||
summary: string,
|
||||
description: string | null,
|
||||
trailers?: ReadonlyArray<ITrailer>
|
||||
context: ICommitContext
|
||||
): Promise<boolean> {
|
||||
return this.appStore._commitIncludedChanges(
|
||||
repository,
|
||||
summary,
|
||||
description,
|
||||
trailers
|
||||
)
|
||||
return this.appStore._commitIncludedChanges(repository, context)
|
||||
}
|
||||
|
||||
/** Change the file's includedness. */
|
||||
|
@ -595,16 +587,28 @@ export class Dispatcher {
|
|||
return this.appStore._mergeBranch(repository, branch, mergeStatus)
|
||||
}
|
||||
|
||||
/** aborts an in-flight merge and refreshes the repository's status */
|
||||
public async abortMerge(repository: Repository) {
|
||||
await this.appStore._abortMerge(repository)
|
||||
await this.appStore._loadStatus(repository)
|
||||
}
|
||||
|
||||
public createMergeCommit(
|
||||
/**
|
||||
* commits an in-flight merge and shows a banner if successful
|
||||
*
|
||||
* @param repository
|
||||
* @param files files to commit. should be all of them in the repository
|
||||
* @param successfulMergeBannerState information for banner to be displayed if merge is successful
|
||||
*/
|
||||
public async createMergeCommit(
|
||||
repository: Repository,
|
||||
files: ReadonlyArray<WorkingDirectoryFileChange>
|
||||
files: ReadonlyArray<WorkingDirectoryFileChange>,
|
||||
successfulMergeBannerState: SuccessfulMergeBannerState
|
||||
) {
|
||||
return this.appStore._createMergeCommit(repository, files)
|
||||
const result = await this.appStore._createMergeCommit(repository, files)
|
||||
if (result !== undefined) {
|
||||
this.appStore._setSuccessfulMergeBannerState(successfulMergeBannerState)
|
||||
}
|
||||
}
|
||||
|
||||
/** Record the given launch stats. */
|
||||
|
@ -1229,12 +1233,15 @@ export class Dispatcher {
|
|||
return this.appStore._updateCompareForm(repository, newState)
|
||||
}
|
||||
|
||||
public resolveCurrentEditor() {
|
||||
return this.appStore._resolveCurrentEditor()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the application state to indicate a conflict is in-progress
|
||||
* as a result of a pull and increments the relevant metric.
|
||||
*/
|
||||
public mergeConflictDetectedFromPull() {
|
||||
this.appStore._mergeConflictDetected()
|
||||
return this.statsStore.recordMergeConflictFromPull()
|
||||
}
|
||||
|
||||
|
@ -1243,7 +1250,6 @@ export class Dispatcher {
|
|||
* as a result of a merge and increments the relevant metric.
|
||||
*/
|
||||
public mergeConflictDetectedFromExplicitMerge() {
|
||||
this.appStore._mergeConflictDetected()
|
||||
return this.statsStore.recordMergeConflictFromExplicitMerge()
|
||||
}
|
||||
|
||||
|
@ -1343,18 +1349,4 @@ export class Dispatcher {
|
|||
public recordAddExistingRepository() {
|
||||
this.statsStore.recordAddExistingRepository()
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the `recordMergeSuccesfulAfterConflicts` metric
|
||||
*/
|
||||
public recordMergeSuccesfulAfterConflicts() {
|
||||
return this.statsStore.recordMergeSuccesAfterConflicts()
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the `recordMergeAbortedAfterConflicts` metric
|
||||
*/
|
||||
public recordMergeAbortedAfterConflicts() {
|
||||
return this.statsStore.recordMergeAbortedAfterConflicts()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -303,7 +303,7 @@ export async function mergeConflictHandler(
|
|||
dispatcher.showPopup({
|
||||
type: PopupType.MergeConflicts,
|
||||
repository,
|
||||
currentBranch: tip.branch.name,
|
||||
ourBranch: tip.branch.name,
|
||||
theirBranch,
|
||||
})
|
||||
|
||||
|
|
|
@ -49,14 +49,11 @@ export async function getAvailableEditors(): Promise<
|
|||
* be found (i.e. it has been removed).
|
||||
*/
|
||||
export async function findEditorOrDefault(
|
||||
name: string | null
|
||||
): Promise<IFoundEditor<ExternalEditor>> {
|
||||
name?: string
|
||||
): Promise<IFoundEditor<ExternalEditor> | null> {
|
||||
const editors = await getAvailableEditors()
|
||||
if (editors.length === 0) {
|
||||
throw new ExternalEditorError(
|
||||
'No suitable editors installed for GitHub Desktop to launch. Install Atom for your platform and restart GitHub Desktop to try again.',
|
||||
{ suggestAtom: true }
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
if (name) {
|
||||
|
|
|
@ -64,5 +64,5 @@ export function enableRecurseSubmodulesFlag(): boolean {
|
|||
|
||||
/** Should the app use the MergeConflictsDialog component and flow? */
|
||||
export function enableMergeConflictsDialog(): boolean {
|
||||
return enableBetaFeatures()
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { ITrailer, mergeTrailers } from './git/interpret-trailers'
|
||||
import { mergeTrailers } from './git/interpret-trailers'
|
||||
import { Repository } from '../models/repository'
|
||||
import { ICommitContext } from '../models/commit'
|
||||
|
||||
/**
|
||||
* Formats a summary and a description into a git-friendly
|
||||
|
@ -16,10 +17,10 @@ import { Repository } from '../models/repository'
|
|||
*/
|
||||
export async function formatCommitMessage(
|
||||
repository: Repository,
|
||||
summary: string,
|
||||
description: string | null,
|
||||
trailers?: ReadonlyArray<ITrailer>
|
||||
context: ICommitContext
|
||||
) {
|
||||
const { summary, description, trailers } = context
|
||||
|
||||
// Git always trim whitespace at the end of commit messages
|
||||
// so we concatenate the summary with the description, ensuring
|
||||
// that they're separated by two newlines. If we don't have a
|
||||
|
|
|
@ -117,3 +117,38 @@ async function checkIfBranchExistsOnRemote(
|
|||
)
|
||||
return result.stdout.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds branches that have a tip equal to the given committish
|
||||
*
|
||||
* @param repository within which to execute the command
|
||||
* @param commitish a sha, HEAD, etc that the branch(es) tip should be
|
||||
* @returns list branch names. null if an error is encountered
|
||||
*/
|
||||
export async function getBranchesPointedAt(
|
||||
repository: Repository,
|
||||
commitish: string
|
||||
): Promise<Array<string> | null> {
|
||||
const args = [
|
||||
'branch',
|
||||
`--points-at=${commitish}`,
|
||||
'--format=%(refname:short)',
|
||||
]
|
||||
// this command has an implicit \n delimiter
|
||||
const { stdout, exitCode } = await git(
|
||||
args,
|
||||
repository.path,
|
||||
'branchPointedAt',
|
||||
{
|
||||
// - 1 is returned if a common ancestor cannot be resolved
|
||||
// - 129 is returned if ref is malformed
|
||||
// "warning: ignoring broken ref refs/remotes/origin/master."
|
||||
successExitCodes: new Set([0, 1, 129]),
|
||||
}
|
||||
)
|
||||
if (exitCode === 1 || exitCode === 129) {
|
||||
return null
|
||||
}
|
||||
// split (and remove trailing element cause its always an empty string)
|
||||
return stdout.split('\n').slice(0, -1)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { git, GitError, IGitResult } from './core'
|
||||
import { git, GitError, parseCommitSHA } from './core'
|
||||
import { stageFiles } from './update-index'
|
||||
import { Repository } from '../../models/repository'
|
||||
import { WorkingDirectoryFileChange } from '../../models/status'
|
||||
|
@ -55,7 +55,35 @@ export async function createMergeCommit(
|
|||
await unstageAll(repository)
|
||||
await stageFiles(repository, files)
|
||||
const result = await git(
|
||||
['commit', '--no-edit'],
|
||||
[
|
||||
'commit',
|
||||
// no-edit here ensures the app does not accidentally invoke the user's editor
|
||||
'--no-edit',
|
||||
// By default Git merge commits do not contain any commentary (which
|
||||
// are lines prefixed with `#`). This works because the Git CLI will
|
||||
// prompt the user to edit the file in `.git/COMMIT_MSG` before
|
||||
// committing, and then it will run `--cleanup=strip`.
|
||||
//
|
||||
// This clashes with our use of `--no-edit` above as Git will now change
|
||||
// it's behavior to invoke `--cleanup=whitespace` as it did not ask
|
||||
// the user to edit the COMMIT_MSG as part of creating a commit.
|
||||
//
|
||||
// From the docs on git-commit (https://git-scm.com/docs/git-commit) I'll
|
||||
// quote the relevant section:
|
||||
// --cleanup=<mode>
|
||||
// strip
|
||||
// Strip leading and trailing empty lines, trailing whitespace,
|
||||
// commentary and collapse consecutive empty lines.
|
||||
// whitespace
|
||||
// Same as `strip` except #commentary is not removed.
|
||||
// default
|
||||
// Same as `strip` if the message is to be edited. Otherwise `whitespace`.
|
||||
//
|
||||
// We should emulate the behavior in this situation because we don't
|
||||
// let the user view or change the commit message before making the
|
||||
// commit.
|
||||
'--cleanup=strip',
|
||||
],
|
||||
repository.path,
|
||||
'createMergeCommit'
|
||||
)
|
||||
|
@ -66,10 +94,6 @@ export async function createMergeCommit(
|
|||
}
|
||||
}
|
||||
|
||||
function parseCommitSHA(result: IGitResult): string {
|
||||
return result.stdout.split(']')[0].split(' ')[1]
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit failures could come from a pre-commit hook rejection.
|
||||
* So display a bit more context than we otherwise would,
|
||||
|
|
|
@ -277,3 +277,11 @@ export const gitNetworkArguments: ReadonlyArray<string> = [
|
|||
'-c',
|
||||
'credential.helper=',
|
||||
]
|
||||
|
||||
/**
|
||||
* Returns the SHA of the passed in IGitResult
|
||||
* @param result
|
||||
*/
|
||||
export function parseCommitSHA(result: IGitResult): string {
|
||||
return result.stdout.split(']')[0].split(' ')[1]
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import { git } from './core'
|
||||
import * as FSE from 'fs-extra'
|
||||
import * as Path from 'path'
|
||||
|
||||
import { git, parseCommitSHA } from './core'
|
||||
import { Repository } from '../../models/repository'
|
||||
import { Branch } from '../../models/branch'
|
||||
import { MergeResult, MergeResultKind } from '../../models/merge'
|
||||
|
@ -9,9 +12,9 @@ import { spawnAndComplete } from './spawn'
|
|||
export async function merge(
|
||||
repository: Repository,
|
||||
branch: string
|
||||
): Promise<true> {
|
||||
await git(['merge', branch], repository.path, 'merge')
|
||||
return true
|
||||
): Promise<string> {
|
||||
const result = await git(['merge', branch], repository.path, 'merge')
|
||||
return parseCommitSHA(result)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -90,3 +93,12 @@ export async function mergeTree(
|
|||
export async function abortMerge(repository: Repository): Promise<void> {
|
||||
await git(['merge', '--abort'], repository.path, 'abortMerge')
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the `.git/MERGE_HEAD` file exists in a repository to confirm
|
||||
* that it is in a conflicted state.
|
||||
*/
|
||||
export async function isMergeHeadSet(repository: Repository): Promise<boolean> {
|
||||
const path = Path.join(repository.path, '.git', 'MERGE_HEAD')
|
||||
return FSE.pathExists(path)
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
ConflictFileStatus,
|
||||
ConflictedFile,
|
||||
} from '../../models/conflicts'
|
||||
import { isMergeHeadSet } from './merge'
|
||||
|
||||
/**
|
||||
* V8 has a limit on the size of string it can create (~256MB), and unless we want to
|
||||
|
@ -54,6 +55,9 @@ export interface IStatusResult {
|
|||
/** true if the repository exists at the given location */
|
||||
readonly exists: boolean
|
||||
|
||||
/** true if repository is in a conflicted state */
|
||||
readonly mergeHeadFound: boolean
|
||||
|
||||
/** the absolute path to the repository's working directory */
|
||||
readonly workingDirectory: WorkingDirectoryStatus
|
||||
}
|
||||
|
@ -217,18 +221,22 @@ export async function getStatus(
|
|||
|
||||
const workingDirectory = WorkingDirectoryStatus.fromFiles([...files.values()])
|
||||
|
||||
const mergeHeadFound = await isMergeHeadSet(repository)
|
||||
|
||||
return {
|
||||
currentBranch,
|
||||
currentTip,
|
||||
currentUpstreamBranch,
|
||||
branchAheadBehind,
|
||||
exists: true,
|
||||
mergeHeadFound,
|
||||
workingDirectory,
|
||||
}
|
||||
}
|
||||
|
||||
function getConflictStatus(
|
||||
path: string,
|
||||
status: FileEntry,
|
||||
conflictState: ConflictState
|
||||
): ConflictFileStatus | null {
|
||||
const { filesWithConflictMarkers, binaryFilePathsInConflicts } = conflictState
|
||||
|
@ -249,6 +257,19 @@ function getConflictStatus(
|
|||
return { kind: 'text', conflictMarkerCount }
|
||||
}
|
||||
|
||||
if (status.kind === 'conflicted') {
|
||||
const { us, them } = status
|
||||
const code = them === us ? us : null
|
||||
const conflictWithoutMarkers =
|
||||
code !== GitStatusEntry.UpdatedButUnmerged &&
|
||||
code !== GitStatusEntry.Modified &&
|
||||
code !== GitStatusEntry.Added
|
||||
|
||||
if (conflictWithoutMarkers) {
|
||||
return { kind: 'text', conflictMarkerCount: null, us, them }
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -286,7 +307,7 @@ function buildStatusMap(
|
|||
files.delete(entry.path)
|
||||
}
|
||||
|
||||
const conflictStatus = getConflictStatus(entry.path, conflictState)
|
||||
const conflictStatus = getConflictStatus(entry.path, status, conflictState)
|
||||
|
||||
// for now we just poke at the existing summary
|
||||
const summary = convertToAppStatus(status, conflictStatus !== null)
|
||||
|
|
|
@ -43,6 +43,12 @@ export interface IHighlightRequest {
|
|||
*/
|
||||
readonly tabSize: number
|
||||
|
||||
/**
|
||||
* The file basename of the path in question as returned
|
||||
* by node's basename() function.
|
||||
*/
|
||||
readonly basename: string
|
||||
|
||||
/**
|
||||
* The file extension of the path in question as returned
|
||||
* by node's extname() function (i.e. with a leading dot).
|
||||
|
|
|
@ -12,6 +12,8 @@ const workerUri = encodePathAsUrl(__dirname, 'highlighter.js')
|
|||
*
|
||||
* @param contents The actual contents which is to be used for
|
||||
* highlighting.
|
||||
* @param basename The file basename of the path in question as returned
|
||||
* by node's basename() function (i.e. without a leading dot).
|
||||
* @param extension The file extension of the path in question as returned
|
||||
* by node's extname() function (i.e. with a leading dot).
|
||||
* @param tabSize The width of a tab character. Defaults to 4. Used by the
|
||||
|
@ -27,6 +29,7 @@ const workerUri = encodePathAsUrl(__dirname, 'highlighter.js')
|
|||
*/
|
||||
export function highlight(
|
||||
contents: string,
|
||||
basename: string,
|
||||
extension: string,
|
||||
tabSize: number,
|
||||
lines: Array<number>
|
||||
|
@ -68,6 +71,7 @@ export function highlight(
|
|||
|
||||
const request: IHighlightRequest = {
|
||||
contents,
|
||||
basename,
|
||||
extension,
|
||||
tabSize,
|
||||
lines,
|
||||
|
|
|
@ -185,8 +185,8 @@ async function findHyper(): Promise<string | null> {
|
|||
const path = commandPieces
|
||||
? commandPieces[2]
|
||||
: localAppData != null
|
||||
? localAppData.concat('\\hyper\\Hyper.exe')
|
||||
: null // fall back to the launcher in install root
|
||||
? localAppData.concat('\\hyper\\Hyper.exe')
|
||||
: null // fall back to the launcher in install root
|
||||
|
||||
if (path == null) {
|
||||
log.debug(
|
||||
|
|
|
@ -711,7 +711,7 @@ export class StatsStore {
|
|||
}
|
||||
|
||||
/** Record when a conflicted merge was successfully completed by the user */
|
||||
public async recordMergeSuccesAfterConflicts(): Promise<void> {
|
||||
public async recordMergeSuccessAfterConflicts(): Promise<void> {
|
||||
return this.updateDailyMeasures(m => ({
|
||||
mergeSuccessAfterConflictsCount: m.mergeSuccessAfterConflictsCount + 1,
|
||||
}))
|
||||
|
|
|
@ -22,7 +22,7 @@ import {
|
|||
import { BranchesTab } from '../../models/branches-tab'
|
||||
import { CloneRepositoryTab } from '../../models/clone-repository-tab'
|
||||
import { CloningRepository } from '../../models/cloning-repository'
|
||||
import { Commit } from '../../models/commit'
|
||||
import { Commit, ICommitContext } from '../../models/commit'
|
||||
import {
|
||||
DiffSelection,
|
||||
DiffSelectionType,
|
||||
|
@ -43,7 +43,6 @@ import {
|
|||
CommittedFileChange,
|
||||
WorkingDirectoryFileChange,
|
||||
WorkingDirectoryStatus,
|
||||
AppFileStatus,
|
||||
} from '../../models/status'
|
||||
import { TipState } from '../../models/tip'
|
||||
import { ICommitMessage } from '../../models/commit-message'
|
||||
|
@ -89,6 +88,7 @@ import {
|
|||
MergeResultStatus,
|
||||
ComparisonMode,
|
||||
SuccessfulMergeBannerState,
|
||||
IConflictState,
|
||||
} from '../app-state'
|
||||
import { caseInsensitiveCompare } from '../compare'
|
||||
import { IGitHubUser } from '../databases/github-user-database'
|
||||
|
@ -122,7 +122,6 @@ import {
|
|||
getWorkingDirectoryDiff,
|
||||
isCoAuthoredByTrailer,
|
||||
mergeTree,
|
||||
ITrailer,
|
||||
pull as pullRepo,
|
||||
push as pushRepo,
|
||||
renameBranch,
|
||||
|
@ -131,6 +130,7 @@ import {
|
|||
appendIgnoreRule,
|
||||
IStatusResult,
|
||||
createMergeCommit,
|
||||
getBranchesPointedAt,
|
||||
} from '../git'
|
||||
import {
|
||||
installGlobalLFSFilters,
|
||||
|
@ -161,6 +161,7 @@ import { AheadBehindUpdater } from './helpers/ahead-behind-updater'
|
|||
import {
|
||||
enableRepoInfoIndicators,
|
||||
enableMergeConflictDetection,
|
||||
enableMergeConflictsDialog,
|
||||
} from '../feature-flag'
|
||||
import { MergeResultKind } from '../../models/merge'
|
||||
import { promiseWithMinimumTimeout } from '../promise'
|
||||
|
@ -173,6 +174,7 @@ import { readEmoji } from '../read-emoji'
|
|||
import { GitStoreCache } from './git-store-cache'
|
||||
import { MergeConflictsErrorContext } from '../git-error-context'
|
||||
import { setNumber, setBoolean, getBoolean, getNumber } from '../local-storage'
|
||||
import { ExternalEditorError } from '../editors/shared'
|
||||
|
||||
/**
|
||||
* As fast-forwarding local branches is proportional to the number of local
|
||||
|
@ -268,6 +270,8 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
|
||||
private selectedExternalEditor?: ExternalEditor
|
||||
|
||||
private resolvedExternalEditor: ExternalEditor | null = null
|
||||
|
||||
/** The user's preferred shell. */
|
||||
private selectedShell = DefaultShell
|
||||
|
||||
|
@ -496,6 +500,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
imageDiffType: this.imageDiffType,
|
||||
selectedShell: this.selectedShell,
|
||||
repositoryFilterText: this.repositoryFilterText,
|
||||
resolvedExternalEditor: this.resolvedExternalEditor,
|
||||
selectedCloneRepositoryTab: this.selectedCloneRepositoryTab,
|
||||
selectedBranchesTab: this.selectedBranchesTab,
|
||||
selectedTheme: this.selectedTheme,
|
||||
|
@ -1470,68 +1475,6 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
}
|
||||
}
|
||||
|
||||
private detectMergeResolution(status: IStatusResult) {
|
||||
const currentBranchName = status.currentBranch
|
||||
if (currentBranchName === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const selection = this.getSelectedState()
|
||||
if (selection === null || selection.type !== SelectionType.Repository) {
|
||||
return
|
||||
}
|
||||
|
||||
const { tip } = selection.state.branchesState
|
||||
|
||||
if (tip.kind !== TipState.Valid) {
|
||||
return
|
||||
}
|
||||
|
||||
const repository = selection.repository
|
||||
const repoState = this.repositoryStateCache.get(repository)
|
||||
const { conflictState } = repoState.changesState
|
||||
|
||||
// conflict state being null means that there are no conflicts
|
||||
if (conflictState === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const previousBranch = conflictState.branch
|
||||
|
||||
// The branch name has changed, so the merge must have been aborted
|
||||
if (previousBranch.name !== currentBranchName) {
|
||||
this.statsStore.recordMergeAbortedAfterConflicts()
|
||||
this.repositoryStateCache.updateChangesState(repository, () => ({
|
||||
conflictState: null,
|
||||
}))
|
||||
this.emitUpdate()
|
||||
return
|
||||
}
|
||||
|
||||
// are there files that have a conflicted or _resolved_ status?
|
||||
const workingDirectioryHasConflicts = status.workingDirectory.files.some(
|
||||
file =>
|
||||
file.status === AppFileStatus.Conflicted ||
|
||||
file.status === AppFileStatus.Resolved
|
||||
)
|
||||
|
||||
if (workingDirectioryHasConflicts) {
|
||||
return
|
||||
}
|
||||
|
||||
if (status.currentTip === previousBranch.tip.sha) {
|
||||
// if the tip is the same, no merge commit was created
|
||||
this.statsStore.recordMergeAbortedAfterConflicts()
|
||||
} else {
|
||||
this.statsStore.recordMergeSuccesAfterConflicts()
|
||||
}
|
||||
|
||||
this.repositoryStateCache.updateChangesState(repository, state => ({
|
||||
conflictState: null,
|
||||
}))
|
||||
this.emitUpdate()
|
||||
}
|
||||
|
||||
/** This shouldn't be called directly. See `Dispatcher`. */
|
||||
public async _loadStatus(
|
||||
repository: Repository,
|
||||
|
@ -1544,7 +1487,6 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
return false
|
||||
}
|
||||
|
||||
this.detectMergeResolution(status)
|
||||
this.repositoryStateCache.updateChangesState(repository, state => {
|
||||
// Populate a map for all files in the current working directory state
|
||||
const filesByID = new Map<string, WorkingDirectoryFileChange>()
|
||||
|
@ -1606,6 +1548,53 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
|
||||
return { workingDirectory, selectedFileIDs, diff }
|
||||
})
|
||||
|
||||
this.repositoryStateCache.updateChangesState(repository, state => {
|
||||
const prevConflictState = state.conflictState
|
||||
const newConflictState = getConflictState(status)
|
||||
|
||||
if (prevConflictState == null && newConflictState == null) {
|
||||
return { conflictState: null }
|
||||
}
|
||||
|
||||
const previousBranchName =
|
||||
prevConflictState != null ? prevConflictState.currentBranch : null
|
||||
const currentBranchName =
|
||||
newConflictState != null ? newConflictState.currentBranch : null
|
||||
|
||||
const branchNameChanged =
|
||||
previousBranchName != null &&
|
||||
currentBranchName != null &&
|
||||
previousBranchName !== currentBranchName
|
||||
|
||||
// The branch name has changed while remaining conflicted -> the merge must have been aborted
|
||||
if (branchNameChanged) {
|
||||
this.statsStore.recordMergeAbortedAfterConflicts()
|
||||
return { conflictState: newConflictState }
|
||||
}
|
||||
|
||||
const { currentTip } = status
|
||||
|
||||
// if the repository is no longer conflicted, what do we think happened?
|
||||
if (
|
||||
prevConflictState != null &&
|
||||
newConflictState == null &&
|
||||
currentTip != null
|
||||
) {
|
||||
const previousTip = prevConflictState.currentTip
|
||||
|
||||
if (previousTip !== currentTip) {
|
||||
this.statsStore.recordMergeSuccessAfterConflicts()
|
||||
} else {
|
||||
this.statsStore.recordMergeAbortedAfterConflicts()
|
||||
}
|
||||
}
|
||||
|
||||
return { conflictState: newConflictState }
|
||||
})
|
||||
|
||||
this._triggerMergeConflictsFlow(repository)
|
||||
|
||||
this.emitUpdate()
|
||||
|
||||
this.updateChangesDiffForCurrentSelection(repository)
|
||||
|
@ -1613,6 +1602,48 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
return true
|
||||
}
|
||||
|
||||
/** starts the conflict resolution flow, if appropriate */
|
||||
private async _triggerMergeConflictsFlow(repository: Repository) {
|
||||
if (!enableMergeConflictsDialog()) {
|
||||
return
|
||||
}
|
||||
|
||||
const alreadyInFlow =
|
||||
this.currentPopup !== null &&
|
||||
(this.currentPopup.type === PopupType.MergeConflicts ||
|
||||
this.currentPopup.type === PopupType.AbortMerge)
|
||||
if (alreadyInFlow) {
|
||||
return
|
||||
}
|
||||
|
||||
const repoState = this.repositoryStateCache.get(repository)
|
||||
const { conflictState } = repoState.changesState
|
||||
if (conflictState === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const possibleTheirsBranches = await getBranchesPointedAt(
|
||||
repository,
|
||||
'MERGE_HEAD'
|
||||
)
|
||||
// null means we encountered an error
|
||||
if (possibleTheirsBranches === null) {
|
||||
return
|
||||
}
|
||||
const theirBranch =
|
||||
possibleTheirsBranches.length === 1
|
||||
? possibleTheirsBranches[0]
|
||||
: undefined
|
||||
|
||||
const ourBranch = conflictState.currentBranch
|
||||
this._showPopup({
|
||||
type: PopupType.MergeConflicts,
|
||||
repository,
|
||||
ourBranch,
|
||||
theirBranch,
|
||||
})
|
||||
}
|
||||
|
||||
/** This shouldn't be called directly. See `Dispatcher`. */
|
||||
public async _changeRepositorySection(
|
||||
repository: Repository,
|
||||
|
@ -1728,8 +1759,8 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
selectableLines
|
||||
)
|
||||
const selectedFile = currentlySelectedFile.withSelection(newSelection)
|
||||
const updatedFiles = changesState.workingDirectory.files.map(
|
||||
f => (f.id === selectedFile.id ? selectedFile : f)
|
||||
const updatedFiles = changesState.workingDirectory.files.map(f =>
|
||||
f.id === selectedFile.id ? selectedFile : f
|
||||
)
|
||||
const workingDirectory = WorkingDirectoryStatus.fromFiles(updatedFiles)
|
||||
|
||||
|
@ -1743,9 +1774,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
/** This shouldn't be called directly. See `Dispatcher`. */
|
||||
public async _commitIncludedChanges(
|
||||
repository: Repository,
|
||||
summary: string,
|
||||
description: string | null,
|
||||
trailers?: ReadonlyArray<ITrailer>
|
||||
context: ICommitContext
|
||||
): Promise<boolean> {
|
||||
const state = this.repositoryStateCache.get(repository)
|
||||
const files = state.changesState.workingDirectory.files
|
||||
|
@ -1757,12 +1786,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
|
||||
const result = await this.isCommitting(repository, () => {
|
||||
return gitStore.performFailableOperation(async () => {
|
||||
const message = await formatCommitMessage(
|
||||
repository,
|
||||
summary,
|
||||
description,
|
||||
trailers
|
||||
)
|
||||
const message = await formatCommitMessage(repository, context)
|
||||
return createCommit(repository, message, selectedFiles)
|
||||
})
|
||||
})
|
||||
|
@ -1777,7 +1801,8 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
this.statsStore.recordPartialCommit()
|
||||
}
|
||||
|
||||
if (trailers != null && trailers.some(isCoAuthoredByTrailer)) {
|
||||
const { trailers } = context
|
||||
if (trailers !== undefined && trailers.some(isCoAuthoredByTrailer)) {
|
||||
this.statsStore.recordCoAuthoredCommit()
|
||||
}
|
||||
|
||||
|
@ -1824,8 +1849,8 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
selection: DiffSelection
|
||||
) {
|
||||
this.repositoryStateCache.updateChangesState(repository, state => {
|
||||
const newFiles = state.workingDirectory.files.map(
|
||||
f => (f.id === file.id ? f.withSelection(selection) : f)
|
||||
const newFiles = state.workingDirectory.files.map(f =>
|
||||
f.id === file.id ? f.withSelection(selection) : f
|
||||
)
|
||||
|
||||
const workingDirectory = WorkingDirectoryStatus.fromFiles(newFiles)
|
||||
|
@ -3097,7 +3122,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
|
||||
if (mergeSuccessful && tip.kind === TipState.Valid) {
|
||||
this._setSuccessfulMergeBannerState({
|
||||
currentBranch: tip.branch.name,
|
||||
ourBranch: tip.branch.name,
|
||||
theirBranch: branch,
|
||||
})
|
||||
}
|
||||
|
@ -3115,9 +3140,9 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
public async _createMergeCommit(
|
||||
repository: Repository,
|
||||
files: ReadonlyArray<WorkingDirectoryFileChange>
|
||||
): Promise<void> {
|
||||
): Promise<string | undefined> {
|
||||
const gitStore = this.gitStoreCache.get(repository)
|
||||
await gitStore.performFailableOperation(() =>
|
||||
return await gitStore.performFailableOperation(() =>
|
||||
createMergeCommit(repository, files)
|
||||
)
|
||||
}
|
||||
|
@ -3151,11 +3176,20 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
|
||||
/** Open a path to a repository or file using the user's configured editor */
|
||||
public async _openInExternalEditor(fullPath: string): Promise<void> {
|
||||
const selectedExternalEditor =
|
||||
this.getState().selectedExternalEditor || null
|
||||
const { selectedExternalEditor } = this.getState()
|
||||
|
||||
try {
|
||||
const match = await findEditorOrDefault(selectedExternalEditor)
|
||||
if (match === null) {
|
||||
this.emitError(
|
||||
new ExternalEditorError(
|
||||
'No suitable editors installed for GitHub Desktop to launch. Install Atom for your platform and restart GitHub Desktop to try again.',
|
||||
{ suggestAtom: true }
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
await launchExternalEditor(fullPath, match)
|
||||
} catch (error) {
|
||||
this.emitError(error)
|
||||
|
@ -3986,33 +4020,13 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
return Promise.resolve()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets conflict state with a non-null value
|
||||
*
|
||||
* The presence of a non-null value signifies
|
||||
* that the repository is in a conflicted state
|
||||
*/
|
||||
public _mergeConflictDetected() {
|
||||
const selection = this.getSelectedState()
|
||||
|
||||
if (selection === null || selection.type !== SelectionType.Repository) {
|
||||
return
|
||||
public async _resolveCurrentEditor() {
|
||||
const match = await findEditorOrDefault(this.selectedExternalEditor)
|
||||
const resolvedExternalEditor = match != null ? match.editor : null
|
||||
if (this.resolvedExternalEditor !== resolvedExternalEditor) {
|
||||
this.resolvedExternalEditor = resolvedExternalEditor
|
||||
this.emitUpdate()
|
||||
}
|
||||
|
||||
const { tip } = selection.state.branchesState
|
||||
|
||||
if (tip.kind !== TipState.Valid) {
|
||||
return
|
||||
}
|
||||
|
||||
const repository = selection.repository
|
||||
|
||||
this.repositoryStateCache.updateChangesState(repository, () => ({
|
||||
conflictState: {
|
||||
branch: tip.branch,
|
||||
},
|
||||
}))
|
||||
this.emitUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4049,3 +4063,22 @@ function getBehindOrDefault(aheadBehind: IAheadBehind | null): number {
|
|||
|
||||
return aheadBehind.behind
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the received status information into a conflict state
|
||||
*/
|
||||
function getConflictState(status: IStatusResult): IConflictState | null {
|
||||
if (!status.mergeHeadFound) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { currentBranch, currentTip } = status
|
||||
if (currentBranch == null || currentTip == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
currentBranch,
|
||||
currentTip,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -515,11 +515,10 @@ export class GitStore extends BaseStore {
|
|||
public async undoCommit(commit: Commit): Promise<void> {
|
||||
// For an initial commit, just delete the reference but leave HEAD. This
|
||||
// will make the branch unborn again.
|
||||
const success = await this.performFailableOperation(
|
||||
() =>
|
||||
commit.parentSHAs.length === 0
|
||||
? this.undoFirstCommit(this.repository)
|
||||
: reset(this.repository, GitResetMode.Mixed, commit.parentSHAs[0])
|
||||
const success = await this.performFailableOperation(() =>
|
||||
commit.parentSHAs.length === 0
|
||||
? this.undoFirstCommit(this.repository)
|
||||
: reset(this.repository, GitResetMode.Mixed, commit.parentSHAs[0])
|
||||
)
|
||||
|
||||
if (success === undefined) {
|
||||
|
@ -561,12 +560,10 @@ export class GitStore extends BaseStore {
|
|||
|
||||
// git-interpret-trailers is really only made for working
|
||||
// with full commit messages so let's start with that
|
||||
const message = await formatCommitMessage(
|
||||
repository,
|
||||
commit.summary,
|
||||
commit.body,
|
||||
[]
|
||||
)
|
||||
const message = await formatCommitMessage(repository, {
|
||||
summary: commit.summary,
|
||||
description: commit.body,
|
||||
})
|
||||
|
||||
// Next we extract any co-authored-by trailers we
|
||||
// can find. We use interpret-trailers for this
|
||||
|
@ -1103,7 +1100,7 @@ export class GitStore extends BaseStore {
|
|||
}
|
||||
|
||||
/** Merge the named branch into the current branch. */
|
||||
public merge(branch: string): Promise<true | undefined> {
|
||||
public merge(branch: string): Promise<string | undefined> {
|
||||
return this.performFailableOperation(() => merge(this.repository, branch), {
|
||||
gitContext: {
|
||||
kind: 'merge',
|
||||
|
|
40
app/src/lib/web-flow-committer.ts
Normal file
40
app/src/lib/web-flow-committer.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { Commit } from '../models/commit'
|
||||
import { GitHubRepository } from '../models/github-repository'
|
||||
import { getDotComAPIEndpoint } from './api'
|
||||
|
||||
/**
|
||||
* Best-effort attempt to figure out if this commit was committed using
|
||||
* the web flow on GitHub.com or GitHub Enterprise. Web flow
|
||||
* commits (such as PR merges) will have a special GitHub committer
|
||||
* with a noreply email address.
|
||||
*
|
||||
* For GitHub.com we can be spot on but for GitHub Enterprise it's
|
||||
* possible we could fail if they've set up a custom smtp host
|
||||
* that doesn't correspond to the hostname.
|
||||
*/
|
||||
export function isWebFlowCommitter(
|
||||
commit: Commit,
|
||||
gitHubRepository: GitHubRepository
|
||||
) {
|
||||
if (!gitHubRepository) {
|
||||
return false
|
||||
}
|
||||
|
||||
const endpoint = gitHubRepository.owner.endpoint
|
||||
const { name, email } = commit.committer
|
||||
|
||||
if (
|
||||
endpoint === getDotComAPIEndpoint() &&
|
||||
name === 'GitHub' &&
|
||||
email === 'noreply@github.com'
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (name === 'GitHub Enterprise') {
|
||||
const host = new URL(endpoint).host.toLowerCase()
|
||||
return email.endsWith(`@${host}`)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -373,8 +373,8 @@ export function buildDefaultMenu({
|
|||
const showLogsLabel = __DARWIN__
|
||||
? 'Show Logs in Finder'
|
||||
: __WIN32__
|
||||
? 'S&how logs in Explorer'
|
||||
: 'S&how logs in your File Manager'
|
||||
? 'S&how logs in Explorer'
|
||||
: 'S&how logs in your File Manager'
|
||||
|
||||
const showLogsItem: Electron.MenuItemConstructorOptions = {
|
||||
label: showLogsLabel,
|
||||
|
@ -506,9 +506,8 @@ function zoom(direction: ZoomDirection): ClickHandler {
|
|||
// zoom factors the value is referring to.
|
||||
const currentZoom = findClosestValue(zoomFactors, rawZoom)
|
||||
|
||||
const nextZoomLevel = zoomFactors.find(
|
||||
f =>
|
||||
direction === ZoomDirection.In ? f > currentZoom : f < currentZoom
|
||||
const nextZoomLevel = zoomFactors.find(f =>
|
||||
direction === ZoomDirection.In ? f > currentZoom : f < currentZoom
|
||||
)
|
||||
|
||||
// If we couldn't find a zoom level (likely due to manual manipulation
|
||||
|
|
|
@ -5,6 +5,7 @@ import { GitAuthor } from './git-author'
|
|||
import { generateGravatarUrl } from '../lib/gravatar'
|
||||
import { getDotComAPIEndpoint } from '../lib/api'
|
||||
import { GitHubRepository } from './github-repository'
|
||||
import { isWebFlowCommitter } from '../lib/web-flow-committer'
|
||||
|
||||
/** The minimum properties we need in order to display a user's avatar. */
|
||||
export interface IAvatarUser {
|
||||
|
@ -83,10 +84,10 @@ export function getAvatarUsersForCommit(
|
|||
)
|
||||
)
|
||||
|
||||
const isWebFlowCommitter =
|
||||
gitHubRepository !== null && commit.isWebFlowCommitter(gitHubRepository)
|
||||
const webFlowCommitter =
|
||||
gitHubRepository !== null && isWebFlowCommitter(commit, gitHubRepository)
|
||||
|
||||
if (!commit.authoredByCommitter && !isWebFlowCommitter) {
|
||||
if (!commit.authoredByCommitter && !webFlowCommitter) {
|
||||
avatarUsers.push(
|
||||
getAvatarUserFromAuthor(gitHubRepository, gitHubUsers, commit.committer)
|
||||
)
|
||||
|
|
|
@ -1,8 +1,22 @@
|
|||
import { CommitIdentity } from './commit-identity'
|
||||
import { ITrailer, isCoAuthoredByTrailer } from '../lib/git/interpret-trailers'
|
||||
import { GitAuthor } from './git-author'
|
||||
import { GitHubRepository } from './github-repository'
|
||||
import { getDotComAPIEndpoint } from '../lib/api'
|
||||
|
||||
/** Grouping of information required to create a commit */
|
||||
export interface ICommitContext {
|
||||
/**
|
||||
* The summary of the commit message (required)
|
||||
*/
|
||||
readonly summary: string
|
||||
/**
|
||||
* Additional details for the commit message (optional)
|
||||
*/
|
||||
readonly description: string | null
|
||||
/**
|
||||
* An optional array of commit trailers (for example Co-Authored-By trailers) which will be appended to the commit message in accordance with the Git trailer configuration.
|
||||
*/
|
||||
readonly trailers?: ReadonlyArray<ITrailer>
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract any Co-Authored-By trailers from an array of arbitrary
|
||||
|
@ -64,38 +78,4 @@ export class Commit {
|
|||
this.author.name === this.committer.name &&
|
||||
this.author.email === this.committer.email
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort attempt to figure out if this commit was committed using
|
||||
* the web flow on GitHub.com or GitHub Enterprise. Web flow
|
||||
* commits (such as PR merges) will have a special GitHub committer
|
||||
* with a noreply email address.
|
||||
*
|
||||
* For GitHub.com we can be spot on but for GitHub Enterprise it's
|
||||
* possible we could fail if they've set up a custom smtp host
|
||||
* that doesn't correspond to the hostname.
|
||||
*/
|
||||
public isWebFlowCommitter(gitHubRepository: GitHubRepository) {
|
||||
if (!gitHubRepository) {
|
||||
return false
|
||||
}
|
||||
|
||||
const endpoint = gitHubRepository.owner.endpoint
|
||||
const { name, email } = this.committer
|
||||
|
||||
if (
|
||||
endpoint === getDotComAPIEndpoint() &&
|
||||
name === 'GitHub' &&
|
||||
email === 'noreply@github.com'
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (this.committer.name === 'GitHub Enterprise') {
|
||||
const host = new URL(endpoint).host.toLowerCase()
|
||||
return email.endsWith(`@${host}`)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,8 +10,15 @@ import { GitStatusEntry, UnmergedEntry } from './status'
|
|||
export type ConflictFileStatus =
|
||||
| {
|
||||
readonly kind: 'text'
|
||||
/** This number should be greater than zero */
|
||||
readonly conflictMarkerCount: number
|
||||
/**
|
||||
* This number should be greater than zero
|
||||
* or null if the file has a non-markered conflict (like added vs removed)
|
||||
*/
|
||||
readonly conflictMarkerCount: number | null
|
||||
/** The state of the file in the current branch */
|
||||
readonly us?: GitStatusEntry
|
||||
/** THe state of the file in the other branch */
|
||||
readonly them?: GitStatusEntry
|
||||
}
|
||||
| {
|
||||
readonly kind: 'binary'
|
||||
|
|
|
@ -124,12 +124,12 @@ export type Popup =
|
|||
| {
|
||||
type: PopupType.MergeConflicts
|
||||
repository: Repository
|
||||
currentBranch: string
|
||||
theirBranch: string
|
||||
ourBranch: string
|
||||
theirBranch?: string
|
||||
}
|
||||
| {
|
||||
type: PopupType.AbortMerge
|
||||
repository: Repository
|
||||
currentBranch: string
|
||||
theirBranch: string
|
||||
ourBranch: string
|
||||
theirBranch?: string
|
||||
}
|
||||
|
|
|
@ -7,28 +7,19 @@ import { ConflictFileStatus } from './conflicts'
|
|||
* The status entry code as reported by Git.
|
||||
*/
|
||||
export enum GitStatusEntry {
|
||||
// M
|
||||
Modified,
|
||||
// A
|
||||
Added,
|
||||
// D
|
||||
Deleted,
|
||||
// R
|
||||
Renamed,
|
||||
// C
|
||||
Copied,
|
||||
// .
|
||||
Unchanged,
|
||||
// ?
|
||||
Untracked,
|
||||
// !
|
||||
Ignored,
|
||||
// U
|
||||
Modified = 'M',
|
||||
Added = 'A',
|
||||
Deleted = 'D',
|
||||
Renamed = 'R',
|
||||
Copied = 'C',
|
||||
Unchanged = '.',
|
||||
Untracked = '?',
|
||||
Ignored = '!',
|
||||
//
|
||||
// While U is a valid code here, we currently mark conflicts as "Modified"
|
||||
// in the application - this will likely be something we need to revisit
|
||||
// down the track as we improve our merge conflict experience
|
||||
UpdatedButUnmerged,
|
||||
UpdatedButUnmerged = 'U',
|
||||
}
|
||||
|
||||
/** The file status as represented in GitHub Desktop. */
|
||||
|
|
|
@ -11,8 +11,8 @@ interface IAbortMergeWarningProps {
|
|||
readonly dispatcher: Dispatcher
|
||||
readonly repository: Repository
|
||||
readonly onDismissed: () => void
|
||||
readonly currentBranch: string
|
||||
readonly theirBranch: string
|
||||
readonly ourBranch: string
|
||||
readonly theirBranch?: string
|
||||
}
|
||||
|
||||
const titleString = 'Confirm abort merge'
|
||||
|
@ -42,11 +42,43 @@ export class AbortMergeWarning extends React.Component<
|
|||
this.props.dispatcher.showPopup({
|
||||
type: PopupType.MergeConflicts,
|
||||
repository: this.props.repository,
|
||||
currentBranch: this.props.currentBranch,
|
||||
ourBranch: this.props.ourBranch,
|
||||
theirBranch: this.props.theirBranch,
|
||||
})
|
||||
}
|
||||
|
||||
private renderTextContent(ourBranch: string, theirBranch?: string) {
|
||||
let firstParagraph
|
||||
|
||||
if (theirBranch !== undefined) {
|
||||
firstParagraph = (
|
||||
<p>
|
||||
{'Are you sure you want to abort merging '}
|
||||
<strong>{theirBranch}</strong>
|
||||
{' into '}
|
||||
<strong>{ourBranch}</strong>?
|
||||
</p>
|
||||
)
|
||||
} else {
|
||||
firstParagraph = (
|
||||
<p>
|
||||
{'Are you sure you want to abort merging into '}
|
||||
<strong>{ourBranch}</strong>?
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="column-left">
|
||||
{firstParagraph}
|
||||
<p>
|
||||
Aborting this merge will take you back to the pre-merge state and the
|
||||
conflicts you've already resolved will still be present.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<Dialog
|
||||
|
@ -58,18 +90,7 @@ export class AbortMergeWarning extends React.Component<
|
|||
>
|
||||
<DialogContent className="content-wrapper">
|
||||
<Octicon symbol={OcticonSymbol.alert} />
|
||||
<div className="column-left">
|
||||
<p>
|
||||
{'Are you sure you want to abort merging '}
|
||||
<strong>{this.props.theirBranch}</strong>
|
||||
{' into '}
|
||||
<strong>{this.props.currentBranch}</strong>?
|
||||
</p>
|
||||
<p>
|
||||
Aborting this merge will take you back to the pre-merge state and
|
||||
the conflicts you've already resolved will still be present.
|
||||
</p>
|
||||
</div>
|
||||
{this.renderTextContent(this.props.ourBranch, this.props.theirBranch)}
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<ButtonGroup>
|
||||
|
|
|
@ -1339,15 +1339,13 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
) {
|
||||
return null
|
||||
}
|
||||
const workingDirectoryStatus =
|
||||
selectedState.state.changesState.workingDirectory
|
||||
// double check that this repository is actually in merge
|
||||
const isInConflictedMerge = workingDirectoryStatus.files.some(
|
||||
file =>
|
||||
file.status === AppFileStatus.Conflicted ||
|
||||
file.status === AppFileStatus.Resolved
|
||||
)
|
||||
if (!isInConflictedMerge) {
|
||||
|
||||
const {
|
||||
workingDirectory,
|
||||
conflictState,
|
||||
} = selectedState.state.changesState
|
||||
|
||||
if (conflictState === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -1355,12 +1353,12 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
<MergeConflictsDialog
|
||||
dispatcher={this.props.dispatcher}
|
||||
repository={popup.repository}
|
||||
status={workingDirectoryStatus}
|
||||
workingDirectory={workingDirectory}
|
||||
onDismissed={this.onPopupDismissed}
|
||||
openFileInExternalEditor={this.openFileInExternalEditor}
|
||||
externalEditorName={this.state.selectedExternalEditor}
|
||||
resolvedExternalEditor={this.state.resolvedExternalEditor}
|
||||
openRepositoryInShell={this.openInShell}
|
||||
currentBranch={popup.currentBranch}
|
||||
ourBranch={popup.ourBranch}
|
||||
theirBranch={popup.theirBranch}
|
||||
/>
|
||||
)
|
||||
|
@ -1398,7 +1396,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
dispatcher={this.props.dispatcher}
|
||||
repository={popup.repository}
|
||||
onDismissed={this.onPopupDismissed}
|
||||
currentBranch={popup.currentBranch}
|
||||
ourBranch={popup.ourBranch}
|
||||
theirBranch={popup.theirBranch}
|
||||
/>
|
||||
)
|
||||
|
@ -1756,14 +1754,24 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
|
||||
// we currently only render one banner at a time
|
||||
private renderBanner(): JSX.Element | null {
|
||||
let banner = null
|
||||
if (this.state.successfulMergeBannerState !== null) {
|
||||
return this.renderSuccessfulMergeBanner(
|
||||
banner = this.renderSuccessfulMergeBanner(
|
||||
this.state.successfulMergeBannerState
|
||||
)
|
||||
} else if (this.state.isUpdateAvailableBannerVisible) {
|
||||
return this.renderUpdateBanner()
|
||||
banner = this.renderUpdateBanner()
|
||||
}
|
||||
return null
|
||||
return (
|
||||
<CSSTransitionGroup
|
||||
transitionName="banner"
|
||||
component="div"
|
||||
transitionEnterTimeout={500}
|
||||
transitionLeaveTimeout={400}
|
||||
>
|
||||
{banner}
|
||||
</CSSTransitionGroup>
|
||||
)
|
||||
}
|
||||
|
||||
private renderUpdateBanner() {
|
||||
|
@ -1775,6 +1783,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
newRelease={updateStore.state.newRelease}
|
||||
releaseNotesLink={releaseNotesUri}
|
||||
onDismissed={this.onUpdateAvailableDismissed}
|
||||
key={'update-available'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -1787,9 +1796,10 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
}
|
||||
return (
|
||||
<SuccessfulMerge
|
||||
currentBranch={successfulMergeBannerState.currentBranch}
|
||||
ourBranch={successfulMergeBannerState.ourBranch}
|
||||
theirBranch={successfulMergeBannerState.theirBranch}
|
||||
onDismissed={this.onSuccessfulMergeDismissed}
|
||||
key={'successful-merge'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ import * as React from 'react'
|
|||
import { Octicon, OcticonSymbol } from '../octicons'
|
||||
|
||||
interface ISuccessfulMergeProps {
|
||||
readonly currentBranch: string
|
||||
readonly theirBranch: string
|
||||
readonly ourBranch: string
|
||||
readonly theirBranch?: string
|
||||
readonly onDismissed: () => void
|
||||
}
|
||||
|
||||
|
@ -13,19 +13,30 @@ export class SuccessfulMerge extends React.Component<
|
|||
> {
|
||||
private timeoutId: NodeJS.Timer | null = null
|
||||
|
||||
private renderMessage(ourBranch: string, theirBranch?: string) {
|
||||
return theirBranch !== undefined ? (
|
||||
<span>
|
||||
{'Successfully merged '}
|
||||
<strong>{theirBranch}</strong>
|
||||
{' into '}
|
||||
<strong>{ourBranch}</strong>
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
{'Successfully merged into '}
|
||||
<strong>{ourBranch}</strong>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div id="successful-merge" className="active">
|
||||
<div id="successful-merge">
|
||||
<div className="green-circle">
|
||||
<Octicon className="check-icon" symbol={OcticonSymbol.check} />
|
||||
</div>
|
||||
<div className="banner-message">
|
||||
<span>
|
||||
{'Successfully merged '}
|
||||
<strong>{this.props.theirBranch}</strong>
|
||||
{' into '}
|
||||
<strong>{this.props.currentBranch}</strong>
|
||||
</span>
|
||||
{this.renderMessage(this.props.ourBranch, this.props.theirBranch)}
|
||||
</div>
|
||||
<div className="close">
|
||||
<a onClick={this.dismiss}>
|
||||
|
@ -39,7 +50,7 @@ export class SuccessfulMerge extends React.Component<
|
|||
public componentDidMount = () => {
|
||||
this.timeoutId = setTimeout(() => {
|
||||
this.dismiss()
|
||||
}, 3250)
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
public componentWillUnmount = () => {
|
||||
|
|
|
@ -32,8 +32,8 @@ export class BranchListItem extends React.Component<IBranchListItemProps, {}> {
|
|||
const infoTitle = isCurrentBranch
|
||||
? 'Current branch'
|
||||
: lastCommitDate
|
||||
? lastCommitDate.toString()
|
||||
: ''
|
||||
? lastCommitDate.toString()
|
||||
: ''
|
||||
return (
|
||||
<div className="branches-list-item">
|
||||
<Octicon className="icon" symbol={icon} />
|
||||
|
|
|
@ -89,7 +89,7 @@ export class BranchesContainer extends React.Component<
|
|||
<Row className="merge-button-row">
|
||||
<Button className="merge-button" onClick={this.onMergeClick}>
|
||||
<Octicon className="icon" symbol={OcticonSymbol.gitMerge} />
|
||||
<span title={`Commit to ${branchName}`}>
|
||||
<span title={`Merge a branch into ${branchName}`}>
|
||||
Choose a branch to merge into <strong>{branchName}</strong>
|
||||
</span>
|
||||
</Button>
|
||||
|
@ -211,6 +211,7 @@ export class BranchesContainer extends React.Component<
|
|||
}
|
||||
|
||||
private onMergeClick = () => {
|
||||
this.props.dispatcher.closeFoldout(FoldoutType.Branch)
|
||||
this.props.dispatcher.showPopup({
|
||||
type: PopupType.MergeBranch,
|
||||
repository: this.props.repository,
|
||||
|
|
|
@ -3,7 +3,6 @@ import * as Path from 'path'
|
|||
|
||||
import { IGitHubUser } from '../../lib/databases'
|
||||
import { Dispatcher } from '../../lib/dispatcher'
|
||||
import { ITrailer } from '../../lib/git/interpret-trailers'
|
||||
import { IMenuItem } from '../../lib/menu-item'
|
||||
import { revealInFileManager } from '../../lib/app-shell'
|
||||
import {
|
||||
|
@ -32,6 +31,7 @@ import { showContextualMenu } from '../main-process-proxy'
|
|||
import { arrayEquals } from '../../lib/equality'
|
||||
import { clipboard } from 'electron'
|
||||
import { basename } from 'path'
|
||||
import { ICommitContext } from '../../models/commit'
|
||||
|
||||
const RowHeight = 29
|
||||
|
||||
|
@ -44,11 +44,7 @@ interface IChangesListProps {
|
|||
readonly onFileSelectionChanged: (rows: ReadonlyArray<number>) => void
|
||||
readonly onIncludeChanged: (path: string, include: boolean) => void
|
||||
readonly onSelectAll: (selectAll: boolean) => void
|
||||
readonly onCreateCommit: (
|
||||
summary: string,
|
||||
description: string | null,
|
||||
trailers?: ReadonlyArray<ITrailer>
|
||||
) => Promise<boolean>
|
||||
readonly onCreateCommit: (context: ICommitContext) => Promise<boolean>
|
||||
readonly onDiscardChanges: (file: WorkingDirectoryFileChange) => void
|
||||
readonly askForConfirmationOnDiscardChanges: boolean
|
||||
readonly onDiscardAllChanges: (
|
||||
|
@ -165,8 +161,8 @@ export class ChangesList extends React.Component<
|
|||
selection === DiffSelectionType.All
|
||||
? true
|
||||
: selection === DiffSelectionType.None
|
||||
? false
|
||||
: null
|
||||
? false
|
||||
: null
|
||||
|
||||
return (
|
||||
<ChangedFile
|
||||
|
@ -237,8 +233,8 @@ export class ChangesList extends React.Component<
|
|||
? `Discard Changes`
|
||||
: `Discard changes`
|
||||
: __DARWIN__
|
||||
? `Discard ${files.length} Selected Changes`
|
||||
: `Discard ${files.length} selected changes`
|
||||
? `Discard ${files.length} Selected Changes`
|
||||
: `Discard ${files.length} selected changes`
|
||||
|
||||
return this.props.askForConfirmationOnDiscardChanges ? `${label}…` : label
|
||||
}
|
||||
|
|
|
@ -19,10 +19,10 @@ import { AuthorInput } from '../lib/author-input'
|
|||
import { FocusContainer } from '../lib/focus-container'
|
||||
import { showContextualMenu } from '../main-process-proxy'
|
||||
import { Octicon, OcticonSymbol } from '../octicons'
|
||||
import { ITrailer } from '../../lib/git/interpret-trailers'
|
||||
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(
|
||||
12,
|
||||
|
@ -34,11 +34,7 @@ const addAuthorIcon = new OcticonSymbol(
|
|||
)
|
||||
|
||||
interface ICommitMessageProps {
|
||||
readonly onCreateCommit: (
|
||||
summary: string,
|
||||
description: string | null,
|
||||
trailers?: ReadonlyArray<ITrailer>
|
||||
) => Promise<boolean>
|
||||
readonly onCreateCommit: (context: ICommitContext) => Promise<boolean>
|
||||
readonly branch: string | null
|
||||
readonly commitAuthor: CommitIdentity | null
|
||||
readonly gitHubUser: IGitHubUser | null
|
||||
|
@ -181,14 +177,18 @@ export class CommitMessage extends React.Component<
|
|||
|
||||
const trailers = this.getCoAuthorTrailers()
|
||||
|
||||
const commitCreated = await this.props.onCreateCommit(
|
||||
// allow single file commit without summary
|
||||
const summaryOrPlaceholder =
|
||||
this.props.singleFileCommit && !this.state.summary
|
||||
? this.props.placeholder
|
||||
: summary,
|
||||
: summary
|
||||
|
||||
const commitContext = {
|
||||
summary: summaryOrPlaceholder,
|
||||
description,
|
||||
trailers
|
||||
)
|
||||
trailers,
|
||||
}
|
||||
|
||||
const commitCreated = await this.props.onCreateCommit(commitContext)
|
||||
|
||||
if (commitCreated) {
|
||||
this.clearCommitMessage()
|
||||
|
@ -282,8 +282,8 @@ export class CommitMessage extends React.Component<
|
|||
? 'Remove Co-Authors'
|
||||
: 'Remove co-authors'
|
||||
: __DARWIN__
|
||||
? 'Add Co-Authors'
|
||||
: 'Add co-authors'
|
||||
? 'Add Co-Authors'
|
||||
: 'Add co-authors'
|
||||
}
|
||||
|
||||
private getAddRemoveCoAuthorsMenuItem(): IMenuItem {
|
||||
|
|
|
@ -20,8 +20,8 @@ export class NoChanges extends React.Component<INoChangesProps, {}> {
|
|||
const opener = __DARWIN__
|
||||
? 'Finder'
|
||||
: __WIN32__
|
||||
? 'Explorer'
|
||||
: 'your File Manager'
|
||||
? 'Explorer'
|
||||
: 'your File Manager'
|
||||
return (
|
||||
<div className="panel blankslate" id="no-changes">
|
||||
<img src={BlankSlateImage} className="blankslate-image" />
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Dispatcher } from '../../lib/dispatcher'
|
|||
import { IGitHubUser } from '../../lib/databases'
|
||||
import { IssuesStore, GitHubUserStore } from '../../lib/stores'
|
||||
import { CommitIdentity } from '../../models/commit-identity'
|
||||
import { Commit } from '../../models/commit'
|
||||
import { Commit, ICommitContext } from '../../models/commit'
|
||||
import { UndoCommit } from './undo-commit'
|
||||
import {
|
||||
IAutocompletionProvider,
|
||||
|
@ -21,7 +21,6 @@ import { ClickSource } from '../lib/list'
|
|||
import { WorkingDirectoryFileChange } from '../../models/status'
|
||||
import { CSSTransitionGroup } from 'react-transition-group'
|
||||
import { openFile } from '../../lib/open-file'
|
||||
import { ITrailer } from '../../lib/git/interpret-trailers'
|
||||
import { Account } from '../../models/account'
|
||||
import { PopupType } from '../../models/popup'
|
||||
|
||||
|
@ -114,16 +113,10 @@ export class ChangesSidebar extends React.Component<IChangesSidebarProps, {}> {
|
|||
}
|
||||
}
|
||||
|
||||
private onCreateCommit = (
|
||||
summary: string,
|
||||
description: string | null,
|
||||
trailers?: ReadonlyArray<ITrailer>
|
||||
): Promise<boolean> => {
|
||||
private onCreateCommit = (context: ICommitContext): Promise<boolean> => {
|
||||
return this.props.dispatcher.commitIncludedChanges(
|
||||
this.props.repository,
|
||||
summary,
|
||||
description,
|
||||
trailers
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -161,7 +161,7 @@ export class CloneGithubRepository extends React.Component<
|
|||
selectedItem={this.state.selectedItem}
|
||||
renderItem={this.renderItem}
|
||||
renderGroupHeader={this.renderGroupHeader}
|
||||
onItemClick={this.onItemClicked}
|
||||
onSelectionChanged={this.onSelectionChanged}
|
||||
invalidationProps={this.state.repositories}
|
||||
groups={this.state.repositories}
|
||||
filterText={this.state.filterText}
|
||||
|
@ -183,9 +183,9 @@ export class CloneGithubRepository extends React.Component<
|
|||
this.setState({ filterText })
|
||||
}
|
||||
|
||||
private onItemClicked = (item: IClonableRepositoryListItem) => {
|
||||
private onSelectionChanged = (item: IClonableRepositoryListItem | null) => {
|
||||
this.setState({ selectedItem: item })
|
||||
this.props.onGitHubRepositorySelected(item.url)
|
||||
this.props.onGitHubRepositorySelected(item != null ? item.url : '')
|
||||
}
|
||||
|
||||
private onChooseDirectory = async () => {
|
||||
|
|
|
@ -179,6 +179,7 @@ export async function highlightContents(
|
|||
const [oldTokens, newTokens] = await Promise.all([
|
||||
highlight(
|
||||
oldContents.toString('utf8'),
|
||||
Path.basename(file.oldPath || file.path),
|
||||
Path.extname(file.oldPath || file.path),
|
||||
tabSize,
|
||||
lineFilters.oldLineFilter
|
||||
|
@ -188,6 +189,7 @@ export async function highlightContents(
|
|||
}),
|
||||
highlight(
|
||||
newContents.toString('utf8'),
|
||||
Path.basename(file.path),
|
||||
Path.extname(file.path),
|
||||
tabSize,
|
||||
lineFilters.newLineFilter
|
||||
|
|
|
@ -194,7 +194,7 @@ export class CommitSummary extends React.Component<
|
|||
|
||||
const expanded = this.props.isExpanded
|
||||
const onClick = expanded ? this.onCollapse : this.onExpand
|
||||
const icon = expanded ? OcticonSymbol.unfold : OcticonSymbol.fold
|
||||
const icon = expanded ? OcticonSymbol.fold : OcticonSymbol.unfold
|
||||
|
||||
return (
|
||||
<a onClick={onClick} className="expander">
|
||||
|
|
|
@ -80,7 +80,7 @@ const startTime = performance.now()
|
|||
|
||||
if (!process.env.TEST_ENV) {
|
||||
/* This is the magic trigger for webpack to go compile
|
||||
* our sass into css and inject it into the DOM. */
|
||||
* our sass into css and inject it into the DOM. */
|
||||
require('../../styles/desktop.scss')
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import * as React from 'react'
|
|||
import { CommitIdentity } from '../../models/commit-identity'
|
||||
import { GitAuthor } from '../../models/git-author'
|
||||
import { GitHubRepository } from '../../models/github-repository'
|
||||
import { isWebFlowCommitter } from '../../lib/web-flow-committer'
|
||||
|
||||
interface ICommitAttributionProps {
|
||||
/**
|
||||
|
@ -80,7 +81,7 @@ export class CommitAttribution extends React.Component<
|
|||
!commit.authoredByCommitter &&
|
||||
!(
|
||||
this.props.gitHubRepository !== null &&
|
||||
commit.isWebFlowCommitter(this.props.gitHubRepository)
|
||||
isWebFlowCommitter(commit, this.props.gitHubRepository)
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
|
@ -10,8 +10,8 @@ export const DefaultEditorLabel = __DARWIN__
|
|||
export const RevealInFileManagerLabel = __DARWIN__
|
||||
? 'Reveal in Finder'
|
||||
: __WIN32__
|
||||
? 'Show in Explorer'
|
||||
: 'Show in your File Manager'
|
||||
? 'Show in Explorer'
|
||||
: 'Show in your File Manager'
|
||||
|
||||
export const TrashNameLabel = __DARWIN__ ? 'Trash' : 'Recycle Bin'
|
||||
|
||||
|
|
|
@ -21,13 +21,51 @@ import { LinkButton } from '../lib/link-button'
|
|||
interface IMergeConflictsDialogProps {
|
||||
readonly dispatcher: Dispatcher
|
||||
readonly repository: Repository
|
||||
readonly status: WorkingDirectoryStatus
|
||||
readonly workingDirectory: WorkingDirectoryStatus
|
||||
readonly onDismissed: () => void
|
||||
readonly openFileInExternalEditor: (path: string) => void
|
||||
readonly externalEditorName?: string
|
||||
readonly resolvedExternalEditor: string | null
|
||||
readonly openRepositoryInShell: (repository: Repository) => void
|
||||
readonly currentBranch: string
|
||||
readonly theirBranch: string
|
||||
readonly ourBranch: string
|
||||
/* `undefined` when we didn't know the branch at the beginning of this flow */
|
||||
readonly theirBranch?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
/** Filter working directory changes for conflicted or resolved files */
|
||||
function getUnmergedFiles(status: WorkingDirectoryStatus) {
|
||||
return status.files.filter(
|
||||
file =>
|
||||
file.status === AppFileStatus.Conflicted ||
|
||||
file.status === AppFileStatus.Resolved
|
||||
)
|
||||
}
|
||||
|
||||
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`
|
||||
}
|
||||
}
|
||||
|
||||
const submitButtonString = 'Commit merge'
|
||||
|
@ -40,13 +78,21 @@ export class MergeConflictsDialog extends React.Component<
|
|||
IMergeConflictsDialogProps,
|
||||
{}
|
||||
> {
|
||||
public async componentDidMount() {
|
||||
this.props.dispatcher.resolveCurrentEditor()
|
||||
}
|
||||
|
||||
/**
|
||||
* commits the merge displays the repository changes tab and dismisses the modal
|
||||
*/
|
||||
private onSubmit = async () => {
|
||||
await this.props.dispatcher.createMergeCommit(
|
||||
this.props.repository,
|
||||
this.props.status.files
|
||||
this.props.workingDirectory.files,
|
||||
{
|
||||
ourBranch: this.props.ourBranch,
|
||||
theirBranch: this.props.theirBranch,
|
||||
}
|
||||
)
|
||||
this.props.dispatcher.setCommitMessage(this.props.repository, null)
|
||||
this.props.dispatcher.changeRepositorySection(
|
||||
|
@ -60,7 +106,7 @@ export class MergeConflictsDialog extends React.Component<
|
|||
* dismisses the modal and shows the abort merge warning modal
|
||||
*/
|
||||
private onCancel = async () => {
|
||||
const anyResolvedFiles = this.getUnmergedFiles().some(
|
||||
const anyResolvedFiles = getUnmergedFiles(this.props.workingDirectory).some(
|
||||
f => f.status === AppFileStatus.Resolved
|
||||
)
|
||||
if (!anyResolvedFiles) {
|
||||
|
@ -71,41 +117,31 @@ export class MergeConflictsDialog extends React.Component<
|
|||
this.props.dispatcher.showPopup({
|
||||
type: PopupType.AbortMerge,
|
||||
repository: this.props.repository,
|
||||
currentBranch: this.props.currentBranch,
|
||||
ourBranch: this.props.ourBranch,
|
||||
theirBranch: this.props.theirBranch,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private calculateConflicts(conflictMarkers: number) {
|
||||
return Math.ceil(conflictMarkers / 3)
|
||||
}
|
||||
|
||||
private renderHeaderTitle(
|
||||
currentBranchName: string,
|
||||
comparisonBranchName: string
|
||||
) {
|
||||
private renderHeaderTitle(ourBranch: string, theirBranch?: string) {
|
||||
if (theirBranch !== undefined) {
|
||||
return (
|
||||
<span>
|
||||
{`Resolve conflicts before merging `}
|
||||
<strong>{theirBranch}</strong>
|
||||
{` into `}
|
||||
<strong>{ourBranch}</strong>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<span>
|
||||
{`Resolve conflicts before merging `}
|
||||
<strong>{comparisonBranchName}</strong>
|
||||
{` into `}
|
||||
<strong>{currentBranchName}</strong>
|
||||
{`Resolve conflicts before merging into `}
|
||||
<strong>{ourBranch}</strong>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
private editorButtonString(editorName: string | undefined) {
|
||||
const defaultEditorString = 'editor'
|
||||
return `Open in ${editorName || defaultEditorString}`
|
||||
}
|
||||
|
||||
private openThisRepositoryInShell = () =>
|
||||
this.props.openRepositoryInShell(this.props.repository)
|
||||
|
||||
|
@ -139,37 +175,68 @@ export class MergeConflictsDialog extends React.Component<
|
|||
private renderConflictedFile(
|
||||
path: string,
|
||||
conflictStatus: ConflictFileStatus,
|
||||
editorName: string | undefined,
|
||||
onOpenEditorClick: () => void
|
||||
): JSX.Element | null {
|
||||
let content = null
|
||||
if (conflictStatus.kind === 'text') {
|
||||
const humanReadableConflicts = this.calculateConflicts(
|
||||
conflictStatus.conflictMarkerCount
|
||||
)
|
||||
const message =
|
||||
humanReadableConflicts === 1
|
||||
? `1 conflict`
|
||||
: `${humanReadableConflicts} conflicts`
|
||||
return (
|
||||
<li className="unmerged-file-status-conflicts">
|
||||
<Octicon symbol={OcticonSymbol.fileCode} className="file-octicon" />
|
||||
<div className="column-left">
|
||||
<PathText path={path} availableWidth={200} />
|
||||
<div className="file-conflicts-status">{message}</div>
|
||||
if (conflictStatus.conflictMarkerCount === null) {
|
||||
content = (
|
||||
<div>
|
||||
<PathText path={path} availableWidth={400} />
|
||||
<div className="command-line-hint">
|
||||
Use command line to resolve this file
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={onOpenEditorClick}>
|
||||
{this.editorButtonString(editorName)}
|
||||
</Button>
|
||||
</li>
|
||||
)
|
||||
} else {
|
||||
const humanReadableConflicts = calculateConflicts(
|
||||
conflictStatus.conflictMarkerCount
|
||||
)
|
||||
const message =
|
||||
humanReadableConflicts === 1
|
||||
? `1 conflict`
|
||||
: `${humanReadableConflicts} conflicts`
|
||||
|
||||
const disabled = this.props.resolvedExternalEditor === null
|
||||
|
||||
const tooltip = editorButtonTooltip(this.props.resolvedExternalEditor)
|
||||
|
||||
content = (
|
||||
<>
|
||||
<div className="column-left">
|
||||
<PathText path={path} availableWidth={200} />
|
||||
<div className="file-conflicts-status">{message}</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={onOpenEditorClick}
|
||||
disabled={disabled}
|
||||
tooltip={tooltip}
|
||||
>
|
||||
{editorButtonString(this.props.resolvedExternalEditor)}
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
} else if (conflictStatus.kind === 'binary') {
|
||||
content = (
|
||||
<div>
|
||||
<PathText path={path} availableWidth={400} />
|
||||
<div className="command-line-hint">
|
||||
Use command line to resolve binary files
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
return content !== null ? (
|
||||
<li className="unmerged-file-status-conflicts">
|
||||
<Octicon symbol={OcticonSymbol.fileCode} className="file-octicon" />
|
||||
{content}
|
||||
</li>
|
||||
) : null
|
||||
}
|
||||
|
||||
private renderUnmergedFile(
|
||||
file: WorkingDirectoryFileChange,
|
||||
editorName: string | undefined,
|
||||
repositoryPath: string
|
||||
file: WorkingDirectoryFileChange
|
||||
): JSX.Element | null {
|
||||
switch (file.status) {
|
||||
case AppFileStatus.Resolved:
|
||||
|
@ -181,12 +248,10 @@ export class MergeConflictsDialog extends React.Component<
|
|||
)
|
||||
}
|
||||
|
||||
return this.renderConflictedFile(
|
||||
file.path,
|
||||
file.conflictStatus,
|
||||
editorName,
|
||||
() =>
|
||||
this.props.openFileInExternalEditor(join(repositoryPath, file.path))
|
||||
return this.renderConflictedFile(file.path, file.conflictStatus, () =>
|
||||
this.props.openFileInExternalEditor(
|
||||
join(this.props.repository.path, file.path)
|
||||
)
|
||||
)
|
||||
default:
|
||||
return null
|
||||
|
@ -194,25 +259,15 @@ export class MergeConflictsDialog extends React.Component<
|
|||
}
|
||||
|
||||
private renderUnmergedFiles(
|
||||
files: Array<WorkingDirectoryFileChange>,
|
||||
editorName: string | undefined,
|
||||
repositoryPath: string
|
||||
files: ReadonlyArray<WorkingDirectoryFileChange>
|
||||
) {
|
||||
return (
|
||||
<ul className="unmerged-file-statuses">
|
||||
{files.map(f => this.renderUnmergedFile(f, editorName, repositoryPath))}
|
||||
{files.map(f => this.renderUnmergedFile(f))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
private getUnmergedFiles() {
|
||||
return this.props.status.files.filter(
|
||||
file =>
|
||||
file.status === AppFileStatus.Conflicted ||
|
||||
file.status === AppFileStatus.Resolved
|
||||
)
|
||||
}
|
||||
|
||||
private renderUnmergedFilesSummary(conflictedFilesCount: number) {
|
||||
// localization, it burns :vampire:
|
||||
const message =
|
||||
|
@ -222,19 +277,49 @@ export class MergeConflictsDialog extends React.Component<
|
|||
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 (
|
||||
<>
|
||||
{this.renderUnmergedFilesSummary(conflictedFilesCount)}
|
||||
{this.renderUnmergedFiles(unmergedFiles)}
|
||||
{this.renderShellLink(this.openThisRepositoryInShell)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const unmergedFiles = this.getUnmergedFiles()
|
||||
const unmergedFiles = getUnmergedFiles(this.props.workingDirectory)
|
||||
const conflictedFilesCount = unmergedFiles.filter(
|
||||
f => f.status === AppFileStatus.Conflicted
|
||||
).length
|
||||
|
||||
const headerTitle = this.renderHeaderTitle(
|
||||
this.props.currentBranch,
|
||||
this.props.ourBranch,
|
||||
this.props.theirBranch
|
||||
)
|
||||
const tooltipString =
|
||||
conflictedFilesCount > 0
|
||||
? 'Resolve all changes before merging'
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
id="merge-conflicts-list"
|
||||
|
@ -244,13 +329,7 @@ export class MergeConflictsDialog extends React.Component<
|
|||
>
|
||||
<DialogHeader title={headerTitle} dismissable={false} />
|
||||
<DialogContent>
|
||||
{this.renderUnmergedFilesSummary(conflictedFilesCount)}
|
||||
{this.renderUnmergedFiles(
|
||||
unmergedFiles,
|
||||
this.props.externalEditorName,
|
||||
this.props.repository.path
|
||||
)}
|
||||
{this.renderShellLink(this.openThisRepositoryInShell)}
|
||||
{this.renderContent(unmergedFiles, conflictedFilesCount)}
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<ButtonGroup>
|
||||
|
|
|
@ -153,8 +153,8 @@ export class RepositoryListItem extends React.Component<
|
|||
const showRepositoryLabel = __DARWIN__
|
||||
? 'Show in Finder'
|
||||
: __WIN32__
|
||||
? 'Show in Explorer'
|
||||
: 'Show in your File Manager'
|
||||
? 'Show in Explorer'
|
||||
: 'Show in your File Manager'
|
||||
|
||||
const items: ReadonlyArray<IMenuItem> = [
|
||||
{
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<style>
|
||||
#error-page {
|
||||
margin: 10px;
|
||||
font-size: 12px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
.stack {
|
||||
font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id='error-page'>
|
||||
<h1>GitHub Desktop failed to launch 💥</h1>
|
||||
|
||||
<p>
|
||||
We encountered a catastrophic error that prevents GitHub Desktop from
|
||||
launching successfully.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Unfortunately this isn't something that can be forwarded to the development
|
||||
team to triage.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Would you mind opening an issue on the GitHub Desktop <a onclick='return onClick(event);' href='https://github.com/desktop/desktop/issues'>issue tracker</a>?
|
||||
The details you need are included below:
|
||||
</p>
|
||||
|
||||
<!--{{content}}-->
|
||||
</div>
|
||||
<script>
|
||||
function onClick(e) {
|
||||
e.preventDefault()
|
||||
const { shell } = require('electron')
|
||||
const url = 'https://github.com/desktop/desktop/issues/new'
|
||||
shell.openExternal(url)
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,9 +1,9 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='UTF-8'>
|
||||
<meta charset="UTF-8" />
|
||||
</head>
|
||||
<body>
|
||||
<div id='desktop-app-container'></div>
|
||||
<div id="desktop-app-container"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -4,14 +4,14 @@
|
|||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 300px;
|
||||
width: 350px;
|
||||
|
||||
& > .tab-bar {
|
||||
border-top: var(--base-border);
|
||||
}
|
||||
|
||||
.branches-list {
|
||||
width: 300px;
|
||||
width: 350px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -228,7 +228,7 @@
|
|||
}
|
||||
|
||||
.no-pull-requests {
|
||||
width: 300px;
|
||||
width: 350px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
border-top: var(--base-border);
|
||||
|
||||
> .focus-container {
|
||||
display: flex;
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
#successful-merge {
|
||||
$banner-height: 30px;
|
||||
height: $banner-height;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
|
@ -7,10 +9,7 @@
|
|||
align-items: center;
|
||||
justify-content: left;
|
||||
padding-left: var(--spacing);
|
||||
border-bottom: var(--base-border);
|
||||
|
||||
height: 0px;
|
||||
transition: height 0.25s;
|
||||
// Prevents the notification banner from decreasing its height
|
||||
// when a large diff is rendered.
|
||||
flex: none;
|
||||
|
@ -27,10 +26,6 @@
|
|||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
&.active {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.green-circle {
|
||||
background-color: var(--color-new);
|
||||
color: var(--background-color);
|
||||
|
@ -46,6 +41,11 @@
|
|||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.close {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
|
@ -66,4 +66,27 @@
|
|||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// animations for entering and leaving
|
||||
&.banner-enter {
|
||||
height: 0px;
|
||||
opacity: 0;
|
||||
|
||||
&.banner-enter-active {
|
||||
height: $banner-height;
|
||||
opacity: 1;
|
||||
transition: height 300ms ease-in-out, opacity 200ms ease-in 300ms;
|
||||
}
|
||||
}
|
||||
|
||||
&.banner-leave {
|
||||
height: $banner-height;
|
||||
opacity: 1;
|
||||
|
||||
&.banner-leave-active {
|
||||
height: 0px;
|
||||
opacity: 0;
|
||||
transition: height 225ms ease-in-out 175ms, opacity 175ms ease-in;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ dialog#merge-conflicts-list {
|
|||
}
|
||||
|
||||
li:last-of-type {
|
||||
margin-bottom: calc(var(--spacing) * 5);
|
||||
margin-bottom: calc(var(--spacing) * 2);
|
||||
}
|
||||
|
||||
li.unmerged-file-status-resolved,
|
||||
|
@ -66,6 +66,7 @@ dialog#merge-conflicts-list {
|
|||
button:last-child,
|
||||
.green-circle:last-child {
|
||||
margin-left: auto;
|
||||
margin-top: var(--spacing);
|
||||
flex-shrink: 0;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
@ -74,8 +75,26 @@ dialog#merge-conflicts-list {
|
|||
.unmerged-file-status-resolved .file-conflicts-status {
|
||||
color: $green;
|
||||
}
|
||||
.unmerged-file-status-conflicts .file-conflicts-status {
|
||||
color: $orange;
|
||||
|
||||
.unmerged-file-status-conflicts {
|
||||
.file-conflicts-status {
|
||||
color: $orange;
|
||||
}
|
||||
|
||||
.command-line-hint {
|
||||
color: $gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.all-conflicts-resolved {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
padding: var(--spacing) 0 var(--spacing-double);
|
||||
|
||||
.message {
|
||||
padding-left: var(--spacing);
|
||||
padding-top: var(--spacing-third);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,27 +7,33 @@ describe('formatCommitMessage', () => {
|
|||
it('always adds trailing newline', async () => {
|
||||
const repo = await setupEmptyRepository()
|
||||
|
||||
expect(await formatCommitMessage(repo, 'test', null)).to.equal('test\n')
|
||||
expect(await formatCommitMessage(repo, 'test', 'test')).to.equal(
|
||||
'test\n\ntest\n'
|
||||
)
|
||||
expect(
|
||||
await formatCommitMessage(repo, { summary: 'test', description: null })
|
||||
).to.equal('test\n')
|
||||
expect(
|
||||
await formatCommitMessage(repo, { summary: 'test', description: 'test' })
|
||||
).to.equal('test\n\ntest\n')
|
||||
})
|
||||
|
||||
it('omits description when null', async () => {
|
||||
const repo = await setupEmptyRepository()
|
||||
expect(await formatCommitMessage(repo, 'test', null)).to.equal('test\n')
|
||||
expect(
|
||||
await formatCommitMessage(repo, { summary: 'test', description: null })
|
||||
).to.equal('test\n')
|
||||
})
|
||||
|
||||
it('omits description when empty string', async () => {
|
||||
const repo = await setupEmptyRepository()
|
||||
expect(await formatCommitMessage(repo, 'test', '')).to.equal('test\n')
|
||||
expect(
|
||||
await formatCommitMessage(repo, { summary: 'test', description: '' })
|
||||
).to.equal('test\n')
|
||||
})
|
||||
|
||||
it('adds two newlines between summary and description', async () => {
|
||||
const repo = await setupEmptyRepository()
|
||||
expect(await formatCommitMessage(repo, 'foo', 'bar')).to.equal(
|
||||
'foo\n\nbar\n'
|
||||
)
|
||||
expect(
|
||||
await formatCommitMessage(repo, { summary: 'foo', description: 'bar' })
|
||||
).to.equal('foo\n\nbar\n')
|
||||
})
|
||||
|
||||
it('appends trailers to a summary-only message', async () => {
|
||||
|
@ -36,7 +42,13 @@ describe('formatCommitMessage', () => {
|
|||
{ token: 'Co-Authored-By', value: 'Markus Olsson <niik@github.com>' },
|
||||
{ token: 'Signed-Off-By', value: 'nerdneha <nerdneha@github.com>' },
|
||||
]
|
||||
expect(await formatCommitMessage(repo, 'foo', null, trailers)).to.equal(
|
||||
expect(
|
||||
await formatCommitMessage(repo, {
|
||||
summary: 'foo',
|
||||
description: null,
|
||||
trailers,
|
||||
})
|
||||
).to.equal(
|
||||
'foo\n\n' +
|
||||
'Co-Authored-By: Markus Olsson <niik@github.com>\n' +
|
||||
'Signed-Off-By: nerdneha <nerdneha@github.com>\n'
|
||||
|
@ -49,7 +61,13 @@ describe('formatCommitMessage', () => {
|
|||
{ token: 'Co-Authored-By', value: 'Markus Olsson <niik@github.com>' },
|
||||
{ token: 'Signed-Off-By', value: 'nerdneha <nerdneha@github.com>' },
|
||||
]
|
||||
expect(await formatCommitMessage(repo, 'foo', 'bar', trailers)).to.equal(
|
||||
expect(
|
||||
await formatCommitMessage(repo, {
|
||||
summary: 'foo',
|
||||
description: 'bar',
|
||||
trailers,
|
||||
})
|
||||
).to.equal(
|
||||
'foo\n\nbar\n\n' +
|
||||
'Co-Authored-By: Markus Olsson <niik@github.com>\n' +
|
||||
'Signed-Off-By: nerdneha <nerdneha@github.com>\n'
|
||||
|
@ -64,12 +82,11 @@ describe('formatCommitMessage', () => {
|
|||
{ token: 'Signed-Off-By', value: 'nerdneha <nerdneha@github.com>' },
|
||||
]
|
||||
expect(
|
||||
await formatCommitMessage(
|
||||
repo,
|
||||
'foo',
|
||||
'Co-Authored-By: Markus Olsson <niik@github.com>',
|
||||
trailers
|
||||
)
|
||||
await formatCommitMessage(repo, {
|
||||
summary: 'foo',
|
||||
description: 'Co-Authored-By: Markus Olsson <niik@github.com>',
|
||||
trailers,
|
||||
})
|
||||
).to.equal(
|
||||
'foo\n\n' +
|
||||
'Co-Authored-By: Markus Olsson <niik@github.com>\n' +
|
||||
|
@ -85,13 +102,12 @@ describe('formatCommitMessage', () => {
|
|||
]
|
||||
|
||||
expect(
|
||||
await formatCommitMessage(
|
||||
repo,
|
||||
'foo',
|
||||
await formatCommitMessage(repo, {
|
||||
summary: 'foo',
|
||||
// note the lack of space after :
|
||||
'Co-Authored-By:Markus Olsson <niik@github.com>',
|
||||
trailers
|
||||
)
|
||||
description: 'Co-Authored-By:Markus Olsson <niik@github.com>',
|
||||
trailers,
|
||||
})
|
||||
).to.equal(
|
||||
'foo\n\n' +
|
||||
'Co-Authored-By: Markus Olsson <niik@github.com>\n' +
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { expect } from 'chai'
|
||||
import { shell } from '../../helpers/test-app-shell'
|
||||
import {
|
||||
setupEmptyRepository,
|
||||
|
@ -14,6 +13,7 @@ import {
|
|||
} from '../../../src/models/tip'
|
||||
import { GitStore } from '../../../src/lib/stores'
|
||||
import { GitProcess } from 'dugite'
|
||||
import { getBranchesPointedAt, createBranch } from '../../../src/lib/git'
|
||||
|
||||
describe('git/branch', () => {
|
||||
describe('tip', () => {
|
||||
|
@ -24,9 +24,9 @@ describe('git/branch', () => {
|
|||
await store.loadStatus()
|
||||
const tip = store.tip
|
||||
|
||||
expect(tip.kind).to.equal(TipState.Unborn)
|
||||
expect(tip.kind).toEqual(TipState.Unborn)
|
||||
const unborn = tip as IUnbornRepository
|
||||
expect(unborn.ref).to.equal('master')
|
||||
expect(unborn.ref).toEqual('master')
|
||||
})
|
||||
|
||||
it('returns correct ref if checkout occurs', async () => {
|
||||
|
@ -38,9 +38,9 @@ describe('git/branch', () => {
|
|||
await store.loadStatus()
|
||||
const tip = store.tip
|
||||
|
||||
expect(tip.kind).to.equal(TipState.Unborn)
|
||||
expect(tip.kind).toEqual(TipState.Unborn)
|
||||
const unborn = tip as IUnbornRepository
|
||||
expect(unborn.ref).to.equal('not-master')
|
||||
expect(unborn.ref).toEqual('not-master')
|
||||
})
|
||||
|
||||
it('returns detached for arbitrary checkout', async () => {
|
||||
|
@ -51,9 +51,9 @@ describe('git/branch', () => {
|
|||
await store.loadStatus()
|
||||
const tip = store.tip
|
||||
|
||||
expect(tip.kind).to.equal(TipState.Detached)
|
||||
expect(tip.kind).toEqual(TipState.Detached)
|
||||
const detached = tip as IDetachedHead
|
||||
expect(detached.currentSha).to.equal(
|
||||
expect(detached.currentSha).toEqual(
|
||||
'2acb028231d408aaa865f9538b1c89de5a2b9da8'
|
||||
)
|
||||
})
|
||||
|
@ -66,10 +66,10 @@ describe('git/branch', () => {
|
|||
await store.loadStatus()
|
||||
const tip = store.tip
|
||||
|
||||
expect(tip.kind).to.equal(TipState.Valid)
|
||||
expect(tip.kind).toEqual(TipState.Valid)
|
||||
const onBranch = tip as IValidBranch
|
||||
expect(onBranch.branch.name).to.equal('commit-with-long-description')
|
||||
expect(onBranch.branch.tip.sha).to.equal(
|
||||
expect(onBranch.branch.name).toEqual('commit-with-long-description')
|
||||
expect(onBranch.branch.tip.sha).toEqual(
|
||||
'dfa96676b65e1c0ed43ca25492252a5e384c8efd'
|
||||
)
|
||||
})
|
||||
|
@ -82,9 +82,9 @@ describe('git/branch', () => {
|
|||
await store.loadStatus()
|
||||
const tip = store.tip
|
||||
|
||||
expect(tip.kind).to.equal(TipState.Valid)
|
||||
expect(tip.kind).toEqual(TipState.Valid)
|
||||
const valid = tip as IValidBranch
|
||||
expect(valid.branch.remote).to.equal('bassoon')
|
||||
expect(valid.branch.remote).toEqual('bassoon')
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -97,12 +97,52 @@ describe('git/branch', () => {
|
|||
await store.loadStatus()
|
||||
const tip = store.tip
|
||||
|
||||
expect(tip.kind).to.equal(TipState.Valid)
|
||||
expect(tip.kind).toEqual(TipState.Valid)
|
||||
|
||||
const valid = tip as IValidBranch
|
||||
expect(valid.branch.remote).to.equal('bassoon')
|
||||
expect(valid.branch.upstream).to.equal('bassoon/master')
|
||||
expect(valid.branch.upstreamWithoutRemote).to.equal('master')
|
||||
expect(valid.branch.remote).toEqual('bassoon')
|
||||
expect(valid.branch.upstream).toEqual('bassoon/master')
|
||||
expect(valid.branch.upstreamWithoutRemote).toEqual('master')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getBranchesPointedAt', () => {
|
||||
let repository: Repository
|
||||
describe('in a local repo', () => {
|
||||
beforeEach(async () => {
|
||||
const path = await setupFixtureRepository('test-repo')
|
||||
repository = new Repository(path, -1, null, false)
|
||||
})
|
||||
|
||||
it('finds one branch name', async () => {
|
||||
const branches = await getBranchesPointedAt(repository, 'HEAD')
|
||||
expect(branches).toHaveLength(1)
|
||||
expect(branches![0]).toEqual('master')
|
||||
})
|
||||
|
||||
it('finds no branch names', async () => {
|
||||
const branches = await getBranchesPointedAt(repository, 'HEAD^')
|
||||
expect(branches).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('returns null on a malformed committish', async () => {
|
||||
const branches = await getBranchesPointedAt(repository, 'MERGE_HEAD')
|
||||
expect(branches).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('in a repo with identical branches', () => {
|
||||
beforeEach(async () => {
|
||||
const path = await setupFixtureRepository('repo-with-multiple-remotes')
|
||||
repository = new Repository(path, -1, null, false)
|
||||
await createBranch(repository, 'other-branch')
|
||||
})
|
||||
it('finds multiple branch names', async () => {
|
||||
const branches = await getBranchesPointedAt(repository, 'HEAD')
|
||||
expect(branches).toHaveLength(2)
|
||||
expect(branches).toContain('other-branch')
|
||||
expect(branches).toContain('master')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -43,7 +43,6 @@ describe('git/checkout', () => {
|
|||
parentSHAs: [],
|
||||
trailers: [],
|
||||
coAuthors: [],
|
||||
isWebFlowCommitter: () => false,
|
||||
},
|
||||
remote: null,
|
||||
}
|
||||
|
|
|
@ -163,33 +163,25 @@ describe('git/status', () => {
|
|||
expect(files[1].path).toBe('docs/OVERVIEW.md')
|
||||
})
|
||||
|
||||
it(
|
||||
'Handles at least 10k untracked files without failing',
|
||||
async () => {
|
||||
const numFiles = 10000
|
||||
const basePath = repository!.path
|
||||
it('Handles at least 10k untracked files without failing', async () => {
|
||||
const numFiles = 10000
|
||||
const basePath = repository!.path
|
||||
|
||||
await mkdir(basePath)
|
||||
await mkdir(basePath)
|
||||
|
||||
// create a lot of files
|
||||
const promises = []
|
||||
for (let i = 0; i < numFiles; i++) {
|
||||
promises.push(
|
||||
FSE.writeFile(
|
||||
path.join(basePath, `test-file-${i}`),
|
||||
'Hey there\n'
|
||||
)
|
||||
)
|
||||
}
|
||||
await Promise.all(promises)
|
||||
// create a lot of files
|
||||
const promises = []
|
||||
for (let i = 0; i < numFiles; i++) {
|
||||
promises.push(
|
||||
FSE.writeFile(path.join(basePath, `test-file-${i}`), 'Hey there\n')
|
||||
)
|
||||
}
|
||||
await Promise.all(promises)
|
||||
|
||||
const status = await getStatusOrThrow(repository!)
|
||||
const files = status.workingDirectory.files
|
||||
expect(files).toHaveLength(numFiles)
|
||||
},
|
||||
// needs a little extra time on CI
|
||||
25000
|
||||
)
|
||||
const status = await getStatusOrThrow(repository!)
|
||||
const files = status.workingDirectory.files
|
||||
expect(files).toHaveLength(numFiles)
|
||||
}, 25000) // needs a little extra time on CI
|
||||
|
||||
it('returns null for directory without a .git directory', async () => {
|
||||
repository = setupEmptyDirectory()
|
||||
|
|
|
@ -4,7 +4,7 @@ platform:
|
|||
- x64
|
||||
|
||||
environment:
|
||||
nodejs_version: '8.11'
|
||||
nodejs_version: '8.12'
|
||||
|
||||
cache:
|
||||
- .eslintcache
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
variables:
|
||||
PYTHON: 'python2.7'
|
||||
|
||||
jobs:
|
||||
- job: Windows
|
||||
pool:
|
||||
|
@ -5,7 +8,7 @@ jobs:
|
|||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '8.11.1'
|
||||
versionSpec: '8.12.0'
|
||||
- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2
|
||||
inputs:
|
||||
versionSpec: '1.5.1'
|
||||
|
@ -28,7 +31,7 @@ jobs:
|
|||
sudo apt-get install -y --no-install-recommends libsecret-1-dev xvfb fakeroot dpkg rpm xz-utils xorriso zsync libxss1 libgconf2-4 libgtk-3-0
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '8.11.1'
|
||||
versionSpec: '8.12.0'
|
||||
- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2
|
||||
inputs:
|
||||
versionSpec: '1.5.1'
|
||||
|
@ -50,7 +53,7 @@ jobs:
|
|||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '8.11.1'
|
||||
versionSpec: '8.12.0'
|
||||
- script: |
|
||||
yarn install --force
|
||||
name: Install
|
||||
|
@ -70,7 +73,7 @@ jobs:
|
|||
sudo apt-get install -y --no-install-recommends libsecret-1-dev
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '8.11.1'
|
||||
versionSpec: '8.12.0'
|
||||
- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2
|
||||
inputs:
|
||||
versionSpec: '1.5.1'
|
||||
|
|
|
@ -1,5 +1,37 @@
|
|||
{
|
||||
"releases": {
|
||||
"1.5.1-beta0": [
|
||||
],
|
||||
"1.5.0": [
|
||||
"[New] Clone, create, or add repositories right from the repository dropdown - #5878",
|
||||
"[New] Drag-and-drop to add local repositories from macOS tray icon - #5048",
|
||||
"[Added] Resolve merge conflicts through a guided flow - #5400",
|
||||
"[Added] Allow merging branches directly from branch dropdown - #5929. Thanks @bruncun!",
|
||||
"[Added] Commit file list now has \"Copy File Path\" context menu action - #2944. Thanks @Amabel!",
|
||||
"[Added] Keyboard shortcut for \"Rename Branch\" menu item - #5964. Thanks @agisilaos!",
|
||||
"[Added] Notify users when a merge is successfully completed - #5851",
|
||||
"[Fixed] \"Compare on GitHub\" menu item enabled when no repository is selected - #6078",
|
||||
"[Fixed] Diff viewer blocks keyboard navigation using reverse tab order - #2794",
|
||||
"[Fixed] Launching Desktop from browser always asks to clone repository - #5913",
|
||||
"[Fixed] Publish dialog displayed on push when repository is already published - #5936",
|
||||
"[Improved] \"Publish Repository\" dialog handles emoji characters - #5980. Thanks @WaleedAshraf!",
|
||||
"[Improved] Avoid repository checks when no path is specified in \"Create Repository\" dialog - #5828. Thanks @JakeHL!",
|
||||
"[Improved] Clarify the direction of merging branches - #5930. Thanks @JQuinnie!",
|
||||
"[Improved] Default commit summary more explanatory and consistent with GitHub.com - #6017. Thanks @Daniel-McCarthy!",
|
||||
"[Improved] Display a more informative message on merge dialog when branch is up to date - #5890",
|
||||
"[Improved] Getting a repository's status only blocks other operations when absolutely necessary - #5952",
|
||||
"[Improved] Display current branch in header of merge dialog - #6027",
|
||||
"[Improved] Sanitize repository name before publishing to GitHub - #3090. Thanks @Daniel-McCarthy!",
|
||||
"[Improved] Show the branch name in \"Update From Default Branch\" menu item - #3018. Thanks @a-golovanov!",
|
||||
"[Improved] Update license and .gitignore templates for initializing a new repository - #6024. Thanks @say25!"
|
||||
],
|
||||
"1.5.0-beta5": [
|
||||
],
|
||||
"1.5.0-beta4": [
|
||||
"[Fixed] \"Compare on GitHub\" menu item enabled when no repository is selected - #6078",
|
||||
"[Fixed] Diff viewer blocks keyboard navigation using reverse tab order - #2794",
|
||||
"[Improved] \"Publish Repository\" dialog handles emoji characters - #5980. Thanks @WaleedAshraf!"
|
||||
],
|
||||
"1.5.0-beta3": [
|
||||
],
|
||||
"1.5.0-beta2": [
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
You will need to install these tools on your machine:
|
||||
|
||||
- Node.js v8.11.4
|
||||
- Node.js v8.12.0
|
||||
- Python 2.7
|
||||
- Xcode and Xcode Command Line Tools (Xcode -> Preferences -> Downloads)
|
||||
|
||||
|
@ -14,11 +14,11 @@ Let's see if you have the right version of `node` installed. Open a terminal and
|
|||
$ node -v
|
||||
```
|
||||
|
||||
If you see an error about being unable to find `node`, that probably means you don't have any Node tools installed. You can install Node LTS (the version we need) from the [Node.js website](https://nodejs.org/en/download/) and restart your shell.
|
||||
If you see an error about being unable to find `node`, that probably means you don't have any Node tools installed. You can install Node 8 from the [Node.js website](https://nodejs.org/download/release/v8.12.0/) and restart your shell.
|
||||
|
||||
If you see the output `v8.11.x` (where `x` is any number), you're good to go.
|
||||
If you see the output `v8.12.0`, you're good to go.
|
||||
|
||||
If you see the output `v10.x.y` you're ahead of what we currently support. We have an outstanding issue building GitHub Desktop with Node 10, and hopefully can resolve this soon. If you don't care about the version you are running, you can install the version from the [Node.js website](https://nodejs.org/en/download/) over the top of your current install.
|
||||
If you see the output `v10.x.y` you're ahead of what we currently support. See [#5876](https://github.com/desktop/desktop/issues/5876) for details about building GitHub Desktop with Node 10, which we can hopefully resolve soon. If you don't care about the version you are running, you can install the version from the [Node.js website](https://nodejs.org/download/release/v8.12.0/) over the top of your current install.
|
||||
|
||||
### I need to use different versions of Node.js in different projects!
|
||||
|
||||
|
@ -46,7 +46,7 @@ $ nvm use
|
|||
$ node -v
|
||||
```
|
||||
|
||||
If you see `v8.11.4`, you're good to go.
|
||||
If you see `v8.12.0`, you're good to go.
|
||||
|
||||
#### Configuring `asdf-nodejs`
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ See [mac-deps-setup.md](./setup-macos.md).
|
|||
|
||||
### Windows
|
||||
|
||||
- [Node.js v8.11.4](https://nodejs.org/dist/v8.11.4/)
|
||||
- [Node.js v8.12.0](https://nodejs.org/dist/v8.12.0/)
|
||||
- *Make sure you allow the Node.js installer to add node to the PATH.*
|
||||
- [Python 2.7](https://www.python.org/downloads/windows/)
|
||||
- *Let Python install into the default suggested path (`c:\Python27`), otherwise you'll have
|
||||
|
@ -121,7 +121,7 @@ versions look similar to the below output:
|
|||
|
||||
```shellsession
|
||||
$ node -v
|
||||
v8.11.4
|
||||
v8.12.0
|
||||
|
||||
$ yarn -v
|
||||
1.9.4
|
||||
|
|
|
@ -92,6 +92,7 @@ time:
|
|||
| | Label name | Description |
|
||||
| ------------------------------- | ------------------ | ----------- |
|
||||
| [:mag_right:][ready-for-review] | `ready-for-review` | Pull Requests that are ready to be reviewed by the maintainers |
|
||||
| [:mag_right:][time-sensitive] | `time-sensitive` | Pull Requests that require review in a more timely manner |
|
||||
|
||||
|
||||
[bug]: https://github.com/desktop/desktop/labels/bug
|
||||
|
@ -116,6 +117,7 @@ time:
|
|||
[ready-for-review]: https://github.com/desktop/desktop/labels/ready-for-review
|
||||
[tech-debt]: https://github.com/desktop/desktop/labels/tech-debt
|
||||
[themes]: https://github.com/desktop/desktop/labels/themes
|
||||
[time-sensitive]: https://github.com/desktop/desktop/labels/time-sensitive
|
||||
[user-research]: https://github.com/desktop/desktop/labels/user-research
|
||||
[website]: https://github.com/desktop/desktop/labels/website
|
||||
[windows]: https://github.com/desktop/desktop/labels/windows
|
||||
|
|
|
@ -8,7 +8,7 @@ For external contributors, we have bundled a developer OAuth application
|
|||
with the Desktop application so that you can complete the sign in flow locally
|
||||
without needing to configure your own application.
|
||||
|
||||
These are listed in [app/webpack.common.js](https://github.com/desktop/desktop/blob/c286d0d513d82b97e1a9c60d44c23020f2ba34d7/app/webpack.common.js#L9-L10).
|
||||
These are listed in [app/app-info.ts](https://github.com/desktop/desktop/blob/85cf9dbae5055cc4f0de9fb4f7046cd32607e877/app/app-info.ts#L9-L10).
|
||||
|
||||
**DO NOT TRUST THIS CLIENT ID AND SECRET! THIS IS ONLY FOR TESTING PURPOSES!!**
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ We introduced syntax highlighted diffs in [#3101](https://github.com/desktop/des
|
|||
|
||||
We currently support syntax highlighting for the following languages.
|
||||
|
||||
JavaScript, JSON, TypeScript, Coffeescript, HTML, CSS, SCSS, LESS, VUE, Markdown, Yaml, XML, Objective-C, Scala, C#, Java, C, C++, Kotlin, Ocaml, F#, sh/bash, Swift, SQL, CYPHER, Go, Perl, PHP, Python, Ruby, Clojure, Rust, Elixir, Haxe, R, JavaServer Pages, PowerShell
|
||||
JavaScript, JSON, TypeScript, Coffeescript, HTML, CSS, SCSS, LESS, VUE, Markdown, Yaml, XML, Objective-C, Scala, C#, Java, C, C++, Kotlin, Ocaml, F#, sh/bash, Swift, SQL, CYPHER, Go, Perl, PHP, Python, Ruby, Clojure, Rust, Elixir, Haxe, R, JavaServer Pages, PowerShell, Docker
|
||||
|
||||
This list was never meant to be exhaustive, we expect to add more languages going forward but this seemed like a good first step.
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ module.exports = {
|
|||
// ignore index files
|
||||
'!**/index.ts',
|
||||
],
|
||||
coverageReporters: ['text-summary', 'json'],
|
||||
coverageReporters: ['text-summary', 'json', 'html'],
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
useBabelrc: true,
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
"test": "yarn test:unit:cov --runInBand && yarn test:script && yarn test:integration",
|
||||
"test:setup": "ts-node -P script/tsconfig.json script/test-setup.ts",
|
||||
"test:review": "ts-node -P script/tsconfig.json script/test-review.ts",
|
||||
"test:report": "codecov -f coverage/*.json",
|
||||
"test:report": "codecov --disable=gcov -f coverage/coverage-final.json",
|
||||
"postinstall": "ts-node -P script/tsconfig.json script/post-install.ts",
|
||||
"start": "cross-env NODE_ENV=development ts-node -P script/tsconfig.json script/start.ts",
|
||||
"start:prod": "cross-env NODE_ENV=production ts-node -P script/tsconfig.json script/start.ts",
|
||||
|
@ -93,7 +93,7 @@
|
|||
"node-sass": "^4.7.2",
|
||||
"octicons": "^7.0.1",
|
||||
"parallel-webpack": "^2.3.0",
|
||||
"prettier": "^1.14.2",
|
||||
"prettier": "1.15.2",
|
||||
"request": "^2.72.0",
|
||||
"rimraf": "^2.5.2",
|
||||
"sass-loader": "^7.0.1",
|
||||
|
|
|
@ -12,7 +12,7 @@ const prettier = process.platform === 'win32' ? 'prettier.cmd' : 'prettier'
|
|||
const prettierPath = Path.join(root, 'node_modules', '.bin', prettier)
|
||||
|
||||
const args = [
|
||||
'**/*.{scss,y{,a}ml}',
|
||||
'**/*.{scss,y{,a}ml,html}',
|
||||
'app/**/*.{ts,tsx}',
|
||||
'script/**/*.ts',
|
||||
'--list-different',
|
||||
|
|
|
@ -8392,10 +8392,10 @@ prettier-linter-helpers@^1.0.0:
|
|||
dependencies:
|
||||
fast-diff "^1.1.2"
|
||||
|
||||
prettier@^1.14.2:
|
||||
version "1.14.2"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.14.2.tgz#0ac1c6e1a90baa22a62925f41963c841983282f9"
|
||||
integrity sha512-McHPg0n1pIke+A/4VcaS2en+pTNjy4xF+Uuq86u/5dyDO59/TtFZtQ708QIRkEZ3qwKz3GVkVa6mpxK/CpB8Rg==
|
||||
prettier@1.15.2:
|
||||
version "1.15.2"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.15.2.tgz#d31abe22afa4351efa14c7f8b94b58bb7452205e"
|
||||
integrity sha512-YgPLFFA0CdKL4Eg2IHtUSjzj/BWgszDHiNQAe0VAIBse34148whfdzLagRL+QiKS+YfK5ftB6X4v/MBw8yCoug==
|
||||
|
||||
pretty-bytes@^1.0.2:
|
||||
version "1.0.4"
|
||||
|
|
Loading…
Reference in a new issue