mirror of
https://github.com/desktop/desktop
synced 2024-11-05 20:49:32 +00:00
Merge branch 'master' into bump-for-new-parser
This commit is contained in:
commit
7bd3f13b4a
41 changed files with 1115 additions and 132 deletions
|
@ -2,3 +2,8 @@ singleQuote: true
|
|||
trailingComma: es5
|
||||
semi: false
|
||||
proseWrap: always
|
||||
|
||||
overrides:
|
||||
- files: "*.scss"
|
||||
options:
|
||||
printWidth: 200
|
|
@ -1,4 +1,4 @@
|
|||
runtime = electron
|
||||
disturl = https://atom.io/download/electron
|
||||
target = 1.8.3
|
||||
target = 1.8.7
|
||||
arch = x64
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"productName": "GitHub Desktop",
|
||||
"bundleID": "com.github.GitHubClient",
|
||||
"companyName": "GitHub, Inc.",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"main": "./main.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -25,7 +25,7 @@
|
|||
"codemirror-mode-elixir": "1.1.1",
|
||||
"deep-equal": "^1.0.1",
|
||||
"dexie": "^2.0.0",
|
||||
"dugite": "^1.66.0",
|
||||
"dugite": "^1.67.0",
|
||||
"electron-window-state": "^4.0.2",
|
||||
"event-kit": "^2.0.0",
|
||||
"file-uri-to-path": "0.0.2",
|
||||
|
@ -59,7 +59,6 @@
|
|||
"devtron": "^1.4.0",
|
||||
"electron-debug": "^1.1.0",
|
||||
"electron-devtools-installer": "^2.2.3",
|
||||
"style-loader": "^0.13.2",
|
||||
"temp": "^0.8.3",
|
||||
"webpack-hot-middleware": "^2.10.0"
|
||||
}
|
||||
|
|
|
@ -121,6 +121,7 @@ const modes: ReadonlyArray<IModeDefinition> = [
|
|||
'.scala': 'text/x-scala',
|
||||
'.sc': 'text/x-scala',
|
||||
'.cs': 'text/x-csharp',
|
||||
'.cake': 'text/x-csharp',
|
||||
'.java': 'text/x-java',
|
||||
'.c': 'text/x-c',
|
||||
'.h': 'text/x-c',
|
||||
|
|
|
@ -27,6 +27,7 @@ import { BranchesTab } from '../models/branches-tab'
|
|||
import { PullRequest } from '../models/pull-request'
|
||||
import { IAuthor } from '../models/author'
|
||||
import { ComparisonCache } from './comparison-cache'
|
||||
import { ApplicationTheme } from '../ui/lib/application-theme'
|
||||
|
||||
export { ICommitMessage }
|
||||
|
||||
|
@ -186,6 +187,11 @@ export interface IAppState {
|
|||
|
||||
/** The currently selected tab for the Branches foldout. */
|
||||
readonly selectedBranchesTab: BranchesTab
|
||||
|
||||
/** Show the diverging notification banner */
|
||||
readonly isDivergingBranchBannerVisible: boolean
|
||||
/** The currently selected appearance (aka theme) */
|
||||
readonly selectedTheme: ApplicationTheme
|
||||
}
|
||||
|
||||
export enum PopupType {
|
||||
|
@ -667,6 +673,16 @@ export interface ICompareState {
|
|||
* A local cache of ahead/behind computations to compare other refs to the current branch
|
||||
*/
|
||||
readonly aheadBehindCache: ComparisonCache
|
||||
|
||||
/**
|
||||
* The best candidate branch to compare the current branch to.
|
||||
* Also includes the ahead/behind info for the inferred branch
|
||||
* relative to the current branch.
|
||||
*/
|
||||
readonly inferredComparisonBranch: {
|
||||
branch: Branch | null
|
||||
aheadBehind: IAheadBehind | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface ICompareFormUpdate {
|
||||
|
|
|
@ -57,6 +57,7 @@ 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'
|
||||
|
||||
/**
|
||||
* An error handler function.
|
||||
|
@ -462,6 +463,13 @@ export class Dispatcher {
|
|||
return this.appStore._setUpdateBannerVisibility(isVisible)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the divering branch notification banner's visibility
|
||||
*/
|
||||
public setDivergingBranchBannerVisibility(isVisible: boolean) {
|
||||
return this.appStore._setDivergingBranchBannerVisibility(isVisible)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the width of the repository sidebar to its default
|
||||
* value. This affects the changes and history sidebar
|
||||
|
@ -1213,4 +1221,18 @@ export class Dispatcher {
|
|||
public recordCompareInitiatedMerge() {
|
||||
return this.appStore._recordCompareInitiatedMerge()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the application-wide theme
|
||||
*/
|
||||
public setSelectedTheme(theme: ApplicationTheme) {
|
||||
return this.appStore._setSelectedTheme(theme)
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of times the user dismisses the diverged branch notification
|
||||
*/
|
||||
public recordDivergingBranchBannerDismissal() {
|
||||
return this.appStore._recordDivergingBranchBannerDismissal()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ export enum ExternalEditor {
|
|||
RubyMine = 'RubyMine',
|
||||
TextMate = 'TextMate',
|
||||
Brackets = 'Brackets',
|
||||
WebStorm = 'WebStorm',
|
||||
}
|
||||
|
||||
export function parse(label: string): ExternalEditor | null {
|
||||
|
@ -47,6 +48,9 @@ export function parse(label: string): ExternalEditor | null {
|
|||
if (label === ExternalEditor.Brackets) {
|
||||
return ExternalEditor.Brackets
|
||||
}
|
||||
if (label === ExternalEditor.WebStorm) {
|
||||
return ExternalEditor.WebStorm
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -77,6 +81,8 @@ function getBundleIdentifiers(editor: ExternalEditor): ReadonlyArray<string> {
|
|||
return ['com.macromates.TextMate']
|
||||
case ExternalEditor.Brackets:
|
||||
return ['io.brackets.appshell']
|
||||
case ExternalEditor.WebStorm:
|
||||
return ['com.jetbrains.WebStorm']
|
||||
default:
|
||||
return assertNever(editor, `Unknown external editor: ${editor}`)
|
||||
}
|
||||
|
@ -113,6 +119,8 @@ function getExecutableShim(
|
|||
return Path.join(installPath, 'Contents', 'Resources', 'mate')
|
||||
case ExternalEditor.Brackets:
|
||||
return Path.join(installPath, 'Contents', 'MacOS', 'Brackets')
|
||||
case ExternalEditor.WebStorm:
|
||||
return Path.join(installPath, 'Contents', 'MacOS', 'WebStorm')
|
||||
default:
|
||||
return assertNever(editor, `Unknown external editor: ${editor}`)
|
||||
}
|
||||
|
@ -158,6 +166,7 @@ export async function getAvailableEditors(): Promise<
|
|||
rubyMinePath,
|
||||
textMatePath,
|
||||
bracketsPath,
|
||||
webStormPath,
|
||||
] = await Promise.all([
|
||||
findApplication(ExternalEditor.Atom),
|
||||
findApplication(ExternalEditor.MacVim),
|
||||
|
@ -169,6 +178,7 @@ export async function getAvailableEditors(): Promise<
|
|||
findApplication(ExternalEditor.RubyMine),
|
||||
findApplication(ExternalEditor.TextMate),
|
||||
findApplication(ExternalEditor.Brackets),
|
||||
findApplication(ExternalEditor.WebStorm),
|
||||
])
|
||||
|
||||
if (atomPath) {
|
||||
|
@ -214,5 +224,9 @@ export async function getAvailableEditors(): Promise<
|
|||
results.push({ editor: ExternalEditor.Brackets, path: bracketsPath })
|
||||
}
|
||||
|
||||
if (webStormPath) {
|
||||
results.push({ editor: ExternalEditor.WebStorm, path: webStormPath })
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { ChildProcess } from 'child_process'
|
||||
import * as Fs from 'fs'
|
||||
import * as Path from 'path'
|
||||
import * as byline from 'byline'
|
||||
|
||||
import { GitProgressParser, IGitProgress, IGitOutput } from './git'
|
||||
|
@ -62,9 +63,17 @@ function createProgressProcessCallback(
|
|||
|
||||
process.on('close', () => {
|
||||
disposable.dispose()
|
||||
// NB: We don't really care about errors deleting the file, but Node
|
||||
// gets kinda bothered if we don't provide a callback.
|
||||
Fs.unlink(lfsProgressPath, () => {})
|
||||
// the order of these callbacks is important because
|
||||
// - unlink can only be done on files
|
||||
// - rmdir can only be done when the directory is empty
|
||||
// - we don't want to surface errors to the user if something goes
|
||||
// wrong (these files can stay in TEMP and be cleaned up eventually)
|
||||
Fs.unlink(lfsProgressPath, err => {
|
||||
if (err == null) {
|
||||
const directory = Path.dirname(lfsProgressPath)
|
||||
Fs.rmdir(directory, () => {})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -58,6 +58,24 @@ export interface IDailyMeasures {
|
|||
|
||||
/** The number of times the user checks out a branch using the PR menu */
|
||||
readonly prBranchCheckouts: number
|
||||
|
||||
/** The number of times the user dismisses the diverged branch notification */
|
||||
readonly divergingBranchBannerDismissal: number
|
||||
|
||||
/** The number of times the user merges from the diverged branch notification merge CTA button */
|
||||
readonly divergingBranchBannerInitatedMerge: number
|
||||
|
||||
/** The number of times the user compares from the diverged branch notification compare CTA button */
|
||||
readonly divergingBranchBannerInitiatedCompare: number
|
||||
|
||||
/**
|
||||
* The number of times the user merges from the compare view after getting to that state
|
||||
* from the diverged branch notification compare CTA button
|
||||
*/
|
||||
readonly divergingBranchBannerInfluencedCompare: number
|
||||
|
||||
/** The number of times the diverged branch notification is displayed */
|
||||
readonly divergingBranchBannerDisplayed: number
|
||||
}
|
||||
|
||||
export class StatsDatabase extends Dexie {
|
||||
|
|
|
@ -7,6 +7,7 @@ import { getOS } from '../get-os'
|
|||
import { getGUID } from './get-guid'
|
||||
import { Repository } from '../../models/repository'
|
||||
import { merge } from '../../lib/merge'
|
||||
import { getPersistedThemeName } from '../../ui/lib/application-theme'
|
||||
|
||||
const StatsEndpoint = 'https://central.github.com/api/usage/desktop'
|
||||
|
||||
|
@ -35,6 +36,11 @@ const DefaultDailyMeasures: IDailyMeasures = {
|
|||
updateFromDefaultBranchMenuCount: 0,
|
||||
mergeIntoCurrentBranchMenuCount: 0,
|
||||
prBranchCheckouts: 0,
|
||||
divergingBranchBannerDismissal: 0,
|
||||
divergingBranchBannerInitatedMerge: 0,
|
||||
divergingBranchBannerInitiatedCompare: 0,
|
||||
divergingBranchBannerInfluencedCompare: 0,
|
||||
divergingBranchBannerDisplayed: 0,
|
||||
}
|
||||
|
||||
interface ICalculatedStats {
|
||||
|
@ -62,6 +68,12 @@ interface ICalculatedStats {
|
|||
/** Is the user logged in with an Enterprise account? */
|
||||
readonly enterpriseAccount: boolean
|
||||
|
||||
/**
|
||||
* The name of the currently selected theme/application
|
||||
* appearance as set at time of stats submission.
|
||||
*/
|
||||
readonly theme: string
|
||||
|
||||
readonly eventType: 'usage'
|
||||
}
|
||||
|
||||
|
@ -177,6 +189,7 @@ export class StatsStore {
|
|||
version: getVersion(),
|
||||
osVersion: getOS(),
|
||||
platform: process.platform,
|
||||
theme: getPersistedThemeName(),
|
||||
...launchStats,
|
||||
...dailyMeasures,
|
||||
...userType,
|
||||
|
@ -360,6 +373,47 @@ export class StatsStore {
|
|||
return this.optOut
|
||||
}
|
||||
|
||||
/** Record that user dismissed diverging branch notification */
|
||||
public async recordDivergingBranchBannerDismissal(): Promise<void> {
|
||||
return this.updateDailyMeasures(m => ({
|
||||
divergingBranchBannerDismissal: m.divergingBranchBannerDismissal + 1,
|
||||
}))
|
||||
}
|
||||
|
||||
/** Record that user initiated a merge from within the notification banner */
|
||||
public async recordDivergingBranchBannerInitatedMerge(): Promise<void> {
|
||||
return this.updateDailyMeasures(m => ({
|
||||
divergingBranchBannerInitatedMerge:
|
||||
m.divergingBranchBannerInitatedMerge + 1,
|
||||
}))
|
||||
}
|
||||
|
||||
/** Record that user initiated a compare from within the notification banner */
|
||||
public async recordDivergingBranchBannerInitiatedCompare(): Promise<void> {
|
||||
return this.updateDailyMeasures(m => ({
|
||||
divergingBranchBannerInitiatedCompare:
|
||||
m.divergingBranchBannerInitiatedCompare + 1,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Record that user initiated a merge after getting to compare view
|
||||
* from within notificatio banner
|
||||
*/
|
||||
public async recordDivergingBranchBannerInfluencedCompare(): Promise<void> {
|
||||
return this.updateDailyMeasures(m => ({
|
||||
divergingBranchBannerInfluencedCompare:
|
||||
m.divergingBranchBannerInfluencedCompare + 1,
|
||||
}))
|
||||
}
|
||||
|
||||
/** Record that the user was shown the notification banner */
|
||||
public async recordDivergingBranchBannerDisplayed(): Promise<void> {
|
||||
return this.updateDailyMeasures(m => ({
|
||||
divergingBranchBannerDisplayed: m.divergingBranchBannerDisplayed + 1,
|
||||
}))
|
||||
}
|
||||
|
||||
/** Post some data to our stats endpoint. */
|
||||
private post(body: object): Promise<Response> {
|
||||
const options: RequestInit = {
|
||||
|
|
|
@ -41,7 +41,11 @@ import {
|
|||
} from '../../lib/repository-matching'
|
||||
import { API, getAccountForEndpoint, IAPIUser } from '../../lib/api'
|
||||
import { caseInsensitiveCompare } from '../compare'
|
||||
import { Branch, eligibleForFastForward } from '../../models/branch'
|
||||
import {
|
||||
Branch,
|
||||
eligibleForFastForward,
|
||||
IAheadBehind,
|
||||
} from '../../models/branch'
|
||||
import { TipState } from '../../models/tip'
|
||||
import { CloningRepository } from '../../models/cloning-repository'
|
||||
import { Commit } from '../../models/commit'
|
||||
|
@ -131,6 +135,12 @@ import { IAuthor } from '../../models/author'
|
|||
import { ComparisonCache } from '../comparison-cache'
|
||||
import { AheadBehindUpdater } from './helpers/ahead-behind-updater'
|
||||
import { enableCompareSidebar } from '../feature-flag'
|
||||
import { inferComparisonBranch } from './helpers/infer-comparison-branch'
|
||||
import {
|
||||
ApplicationTheme,
|
||||
getPersistedTheme,
|
||||
setPersistedTheme,
|
||||
} from '../../ui/lib/application-theme'
|
||||
|
||||
/**
|
||||
* Enum used by fetch to determine if
|
||||
|
@ -263,6 +273,8 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
private selectedCloneRepositoryTab = CloneRepositoryTab.DotCom
|
||||
|
||||
private selectedBranchesTab = BranchesTab.Branches
|
||||
private selectedTheme = ApplicationTheme.Light
|
||||
private isDivergingBranchBannerVisible = false
|
||||
|
||||
public constructor(
|
||||
gitHubUserStore: GitHubUserStore,
|
||||
|
@ -446,6 +458,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
allBranches: new Array<Branch>(),
|
||||
recentBranches: new Array<Branch>(),
|
||||
defaultBranch: null,
|
||||
inferredComparisonBranch: { branch: null, aheadBehind: null },
|
||||
},
|
||||
commitAuthor: null,
|
||||
gitHubUsers: new Map<string, IGitHubUser>(),
|
||||
|
@ -598,6 +611,8 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
repositoryFilterText: this.repositoryFilterText,
|
||||
selectedCloneRepositoryTab: this.selectedCloneRepositoryTab,
|
||||
selectedBranchesTab: this.selectedBranchesTab,
|
||||
isDivergingBranchBannerVisible: this.isDivergingBranchBannerVisible,
|
||||
selectedTheme: this.selectedTheme,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -746,8 +761,8 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
|
||||
const state = this.getRepositoryState(repository)
|
||||
|
||||
const branchesState = state.branchesState
|
||||
const tip = branchesState.tip
|
||||
const { branchesState, compareState } = state
|
||||
const { tip, currentPullRequest } = branchesState
|
||||
const currentBranch = tip.kind === TipState.Valid ? tip.branch : null
|
||||
|
||||
const allBranches =
|
||||
|
@ -769,16 +784,60 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
? cachedDefaultBranch
|
||||
: null
|
||||
|
||||
let inferredBranch: Branch | null = null
|
||||
let aheadBehindOfInferredBranch: IAheadBehind | null = null
|
||||
if (tip.kind === TipState.Valid && compareState.aheadBehindCache !== null) {
|
||||
inferredBranch = await inferComparisonBranch(
|
||||
repository,
|
||||
allBranches,
|
||||
currentPullRequest,
|
||||
tip.branch,
|
||||
getRemotes,
|
||||
compareState.aheadBehindCache
|
||||
)
|
||||
|
||||
if (inferredBranch !== null) {
|
||||
aheadBehindOfInferredBranch = compareState.aheadBehindCache.get(
|
||||
tip.branch.tip.sha,
|
||||
inferredBranch.tip.sha
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
this.updateCompareState(repository, state => ({
|
||||
allBranches,
|
||||
recentBranches,
|
||||
defaultBranch,
|
||||
inferredComparisonBranch: {
|
||||
branch: inferredBranch,
|
||||
aheadBehind: aheadBehindOfInferredBranch,
|
||||
},
|
||||
}))
|
||||
|
||||
const compareState = state.compareState
|
||||
// we only want to show the banner when the the number
|
||||
// commits behind has changed since the last it was visible
|
||||
if (
|
||||
inferredBranch !== null &&
|
||||
aheadBehindOfInferredBranch !== null &&
|
||||
aheadBehindOfInferredBranch.behind > 0
|
||||
) {
|
||||
const prevInferredBranchState =
|
||||
state.compareState.inferredComparisonBranch
|
||||
if (
|
||||
prevInferredBranchState.aheadBehind === null ||
|
||||
prevInferredBranchState.aheadBehind.behind !==
|
||||
aheadBehindOfInferredBranch.behind
|
||||
) {
|
||||
this._setDivergingBranchBannerVisibility(true)
|
||||
}
|
||||
} else if (
|
||||
inferComparisonBranch !== null ||
|
||||
aheadBehindOfInferredBranch === null
|
||||
) {
|
||||
this._setDivergingBranchBannerVisibility(false)
|
||||
}
|
||||
|
||||
const cachedState = compareState.formState
|
||||
|
||||
const action =
|
||||
initialAction != null ? initialAction : getInitialAction(cachedState)
|
||||
this._executeCompare(repository, action)
|
||||
|
@ -882,9 +941,29 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
}
|
||||
|
||||
/** This shouldn't be called directly. See `Dispatcher`. */
|
||||
public _loadNextHistoryBatch(repository: Repository): Promise<void> {
|
||||
public async _loadNextHistoryBatch(repository: Repository): Promise<void> {
|
||||
const gitStore = this.getGitStore(repository)
|
||||
return gitStore.loadNextHistoryBatch()
|
||||
|
||||
if (enableCompareSidebar()) {
|
||||
const state = this.getRepositoryState(repository)
|
||||
const { formState } = state.compareState
|
||||
if (formState.kind === ComparisonView.None) {
|
||||
const commits = state.compareState.commitSHAs
|
||||
const lastCommitSha = commits[commits.length - 1]
|
||||
|
||||
const newCommits = await gitStore.loadCommitBatch(lastCommitSha)
|
||||
if (newCommits == null) {
|
||||
return
|
||||
}
|
||||
|
||||
this.updateCompareState(repository, state => ({
|
||||
commitSHAs: commits.concat(newCommits),
|
||||
}))
|
||||
this.emitUpdate()
|
||||
}
|
||||
} else {
|
||||
return gitStore.loadNextHistoryBatch()
|
||||
}
|
||||
}
|
||||
|
||||
/** This shouldn't be called directly. See `Dispatcher`. */
|
||||
|
@ -1310,6 +1389,8 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
? imageDiffTypeDefault
|
||||
: parseInt(imageDiffTypeValue)
|
||||
|
||||
this.selectedTheme = getPersistedTheme()
|
||||
|
||||
this.emitUpdateNow()
|
||||
|
||||
this.accountsStore.refresh()
|
||||
|
@ -2933,6 +3014,18 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
this.emitUpdate()
|
||||
}
|
||||
|
||||
public _setDivergingBranchBannerVisibility(visible: boolean) {
|
||||
if (this.isDivergingBranchBannerVisible !== visible) {
|
||||
this.isDivergingBranchBannerVisible = visible
|
||||
|
||||
if (visible) {
|
||||
this._recordDivergingBranchBannerDisplayed()
|
||||
}
|
||||
|
||||
this.emitUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
public _reportStats() {
|
||||
return this.statsStore.reportStats(this.accounts, this.repositories)
|
||||
}
|
||||
|
@ -3665,6 +3758,31 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
public _recordCompareInitiatedMerge() {
|
||||
this.statsStore.recordCompareInitiatedMerge()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the application-wide theme
|
||||
*/
|
||||
public _setSelectedTheme(theme: ApplicationTheme) {
|
||||
setPersistedTheme(theme)
|
||||
this.selectedTheme = theme
|
||||
this.emitUpdate()
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of times the user dismisses the diverged branch notification
|
||||
*/
|
||||
public _recordDivergingBranchBannerDismissal() {
|
||||
this.statsStore.recordDivergingBranchBannerDismissal()
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of times the user showne the diverged branch notification
|
||||
*/
|
||||
public _recordDivergingBranchBannerDisplayed() {
|
||||
this.statsStore.recordDivergingBranchBannerDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
function forkPullRequestRemoteName(remoteName: string) {
|
||||
|
|
|
@ -256,6 +256,32 @@ export class GitStore extends BaseStore {
|
|||
this.emitUpdate()
|
||||
}
|
||||
|
||||
/** Load a batch of commits from the repository, using the last known commit in the list. */
|
||||
public async loadCommitBatch(lastSHA: string) {
|
||||
if (this.requestsInFight.has(LoadingHistoryRequestKey)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const requestKey = `history/compare/${lastSHA}`
|
||||
if (this.requestsInFight.has(requestKey)) {
|
||||
return null
|
||||
}
|
||||
|
||||
this.requestsInFight.add(requestKey)
|
||||
|
||||
const commits = await this.performFailableOperation(() =>
|
||||
getCommits(this.repository, `${lastSHA}^`, CommitBatchSize)
|
||||
)
|
||||
|
||||
this.requestsInFight.delete(requestKey)
|
||||
if (!commits) {
|
||||
return null
|
||||
}
|
||||
|
||||
this.storeCommits(commits, false)
|
||||
return commits.map(c => c.sha)
|
||||
}
|
||||
|
||||
/** The list of ordered SHAs. */
|
||||
public get history(): ReadonlyArray<string> {
|
||||
return this._history
|
||||
|
|
149
app/src/lib/stores/helpers/infer-comparison-branch.ts
Normal file
149
app/src/lib/stores/helpers/infer-comparison-branch.ts
Normal file
|
@ -0,0 +1,149 @@
|
|||
import { Branch } from '../../../models/branch'
|
||||
import { PullRequest } from '../../../models/pull-request'
|
||||
import { GitHubRepository } from '../../../models/github-repository'
|
||||
import { IRemote } from '../../../models/remote'
|
||||
import { Repository } from '../../../models/repository'
|
||||
import { ComparisonCache } from '../../comparison-cache'
|
||||
|
||||
type RemotesGetter = (repository: Repository) => Promise<ReadonlyArray<IRemote>>
|
||||
|
||||
/**
|
||||
* Infers which branch to use as the comparison branch
|
||||
*
|
||||
* The branch returned is determined by the following conditions:
|
||||
* 1. Given a pull request -> target branch of PR
|
||||
* 2. Given a forked repository -> default branch on `upstream`
|
||||
* 3. Given a hosted repository -> default branch on `origin`
|
||||
* 4. Fallback -> `master` branch
|
||||
*
|
||||
* @param repository The repository the branch belongs to
|
||||
* @param branches The list of all branches for the repository
|
||||
* @param currentPullRequest The pull request to use for finding the branch
|
||||
* @param currentBranch The branch we want the parent of
|
||||
* @param getRemotes callback used to get all remotes for the current repository
|
||||
* @param comparisonCache cache used to get the number of commits ahead/behind the current branch is from another branch
|
||||
*/
|
||||
export async function inferComparisonBranch(
|
||||
repository: Repository,
|
||||
branches: ReadonlyArray<Branch>,
|
||||
currentPullRequest: PullRequest | null,
|
||||
currentBranch: Branch | null,
|
||||
getRemotes: RemotesGetter,
|
||||
comparisonCache: ComparisonCache
|
||||
): Promise<Branch | null> {
|
||||
if (currentPullRequest !== null) {
|
||||
return getTargetBranchOfPullRequest(branches, currentPullRequest)
|
||||
}
|
||||
|
||||
const ghRepo = repository.gitHubRepository
|
||||
if (ghRepo !== null) {
|
||||
return ghRepo.fork === true && currentBranch !== null
|
||||
? getDefaultBranchOfFork(
|
||||
repository,
|
||||
branches,
|
||||
currentBranch,
|
||||
getRemotes,
|
||||
comparisonCache
|
||||
)
|
||||
: getDefaultBranchOfGitHubRepo(branches, ghRepo)
|
||||
}
|
||||
|
||||
return getMasterBranch(branches)
|
||||
}
|
||||
|
||||
function getMasterBranch(branches: ReadonlyArray<Branch>): Branch | null {
|
||||
return findBranch(branches, 'master')
|
||||
}
|
||||
|
||||
function getDefaultBranchOfGitHubRepo(
|
||||
branches: ReadonlyArray<Branch>,
|
||||
ghRepository: GitHubRepository
|
||||
): Branch | null {
|
||||
if (ghRepository.defaultBranch === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return findBranch(branches, ghRepository.defaultBranch)
|
||||
}
|
||||
|
||||
function getTargetBranchOfPullRequest(
|
||||
branches: ReadonlyArray<Branch>,
|
||||
pr: PullRequest
|
||||
): Branch | null {
|
||||
return findBranch(branches, pr.base.ref)
|
||||
}
|
||||
|
||||
/**
|
||||
* For `inferComparisonBranch` case where inferring for a forked repository
|
||||
*
|
||||
* Returns the default branch of the fork if it's ahead of `currentBranch`.
|
||||
* Otherwise, the default branch of the parent is returned.
|
||||
*/
|
||||
async function getDefaultBranchOfFork(
|
||||
repository: Repository,
|
||||
branches: ReadonlyArray<Branch>,
|
||||
currentBranch: Branch,
|
||||
getRemotes: RemotesGetter,
|
||||
comparisonCache: ComparisonCache
|
||||
): Promise<Branch | null> {
|
||||
// this is guaranteed to exist since this function
|
||||
// is only called if the ghRepo is not null
|
||||
const ghRepo = repository.gitHubRepository!
|
||||
const defaultBranch = getDefaultBranchOfGitHubRepo(branches, ghRepo)
|
||||
if (defaultBranch === null) {
|
||||
return getMasterBranch(branches)
|
||||
}
|
||||
|
||||
const aheadBehind = comparisonCache.get(
|
||||
currentBranch.tip.sha,
|
||||
defaultBranch.tip.sha
|
||||
)
|
||||
|
||||
// we want to return the default branch of the fork if it's ahead
|
||||
// of the current branch; see https://github.com/desktop/desktop/issues/4766#issue-325764371
|
||||
if (aheadBehind !== null && aheadBehind.ahead > 0) {
|
||||
return defaultBranch
|
||||
}
|
||||
|
||||
const potentialDefault = await getDefaultBranchOfForkedGitHubRepo(
|
||||
repository,
|
||||
branches,
|
||||
getRemotes
|
||||
)
|
||||
|
||||
return potentialDefault
|
||||
}
|
||||
|
||||
async function getDefaultBranchOfForkedGitHubRepo(
|
||||
repository: Repository,
|
||||
branches: ReadonlyArray<Branch>,
|
||||
getRemotes: RemotesGetter
|
||||
): Promise<Branch | null> {
|
||||
const parentRepo =
|
||||
repository.gitHubRepository !== null
|
||||
? repository.gitHubRepository.parent
|
||||
: null
|
||||
|
||||
if (parentRepo === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const remotes = await getRemotes(repository)
|
||||
const remote = remotes.find(r => r.url === parentRepo.cloneURL)
|
||||
|
||||
if (remote === undefined) {
|
||||
log.warn(`Could not find remote with URL ${parentRepo.cloneURL}.`)
|
||||
return null
|
||||
}
|
||||
|
||||
const branchToFind = `${remote.name}/${parentRepo.defaultBranch}`
|
||||
|
||||
return findBranch(branches, branchToFind)
|
||||
}
|
||||
|
||||
function findBranch(
|
||||
branches: ReadonlyArray<Branch>,
|
||||
name: string
|
||||
): Branch | null {
|
||||
return branches.find(b => b.name === name) || null
|
||||
}
|
|
@ -67,7 +67,6 @@ export class PullRequestStore extends TypedBaseStore<GitHubRepository> {
|
|||
this.emitUpdate(githubRepo)
|
||||
} catch (error) {
|
||||
log.warn(`Error refreshing pull requests for '${repository.name}'`, error)
|
||||
this.emitError(error)
|
||||
} finally {
|
||||
this.updateActiveFetchCount(githubRepo, Decrement)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export enum PreferencesTab {
|
||||
Accounts = 0,
|
||||
Git,
|
||||
Advanced,
|
||||
Git = 1,
|
||||
Appearance = 2,
|
||||
Advanced = 3,
|
||||
}
|
||||
|
|
59
app/src/ui/app-theme.tsx
Normal file
59
app/src/ui/app-theme.tsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
import * as React from 'react'
|
||||
import { ApplicationTheme, getThemeName } from './lib/application-theme'
|
||||
|
||||
interface IAppThemeProps {
|
||||
readonly theme: ApplicationTheme
|
||||
}
|
||||
|
||||
/**
|
||||
* A pseudo-component responsible for adding the applicable CSS
|
||||
* class names to the body tag in order to apply the currently
|
||||
* selected theme.
|
||||
*
|
||||
* This component is a PureComponent, meaning that it'll only
|
||||
* render when its props changes (shallow comparison).
|
||||
*
|
||||
* This component does not render anything into the DOM, it's
|
||||
* purely (a)busing the component lifecycle to manipulate the
|
||||
* body class list.
|
||||
*/
|
||||
export class AppTheme extends React.PureComponent<IAppThemeProps> {
|
||||
public componentDidMount() {
|
||||
this.ensureTheme()
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
this.ensureTheme()
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.clearThemes()
|
||||
}
|
||||
|
||||
private ensureTheme() {
|
||||
const newThemeClassName = `theme-${getThemeName(this.props.theme)}`
|
||||
const body = document.body
|
||||
|
||||
if (body.classList.contains(newThemeClassName)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.clearThemes()
|
||||
|
||||
body.classList.add(newThemeClassName)
|
||||
}
|
||||
|
||||
private clearThemes() {
|
||||
const body = document.body
|
||||
|
||||
for (const className of body.classList) {
|
||||
if (className.startsWith('theme-')) {
|
||||
body.classList.remove(className)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return null
|
||||
}
|
||||
}
|
|
@ -87,6 +87,8 @@ import { InitializeLFS, AttributeMismatch } from './lfs'
|
|||
import { UpstreamAlreadyExists } from './upstream-already-exists'
|
||||
import { DeletePullRequest } from './delete-branch/delete-pull-request-dialog'
|
||||
import { MergeConflictsWarning } from './merge-conflicts'
|
||||
import { AppTheme } from './app-theme'
|
||||
import { ApplicationTheme } from './lib/application-theme'
|
||||
|
||||
/** The interval at which we should check for updates. */
|
||||
const UpdateCheckInterval = 1000 * 60 * 60 * 4
|
||||
|
@ -1010,6 +1012,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
enterpriseAccount={this.getEnterpriseAccount()}
|
||||
onDismissed={this.onPopupDismissed}
|
||||
selectedShell={this.state.selectedShell}
|
||||
selectedTheme={this.state.selectedTheme}
|
||||
/>
|
||||
)
|
||||
case PopupType.MergeBranch: {
|
||||
|
@ -1391,6 +1394,9 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
onSelectionChanged={this.onSelectionChanged}
|
||||
repositories={this.state.repositories}
|
||||
localRepositoryStateLookup={this.state.localRepositoryStateLookup}
|
||||
askForConfirmationOnRemoveRepository={
|
||||
this.state.askForConfirmationOnRepositoryRemoval
|
||||
}
|
||||
onRemoveRepository={this.removeRepository}
|
||||
onOpenInShell={this.openInShell}
|
||||
onShowRepository={this.showRepository}
|
||||
|
@ -1648,26 +1654,27 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
}
|
||||
|
||||
if (selectedState.type === SelectionType.Repository) {
|
||||
const externalEditorLabel = this.state.selectedExternalEditor
|
||||
const externalEditorLabel = state.selectedExternalEditor
|
||||
|
||||
return (
|
||||
<RepositoryView
|
||||
repository={selectedState.repository}
|
||||
state={selectedState.state}
|
||||
dispatcher={this.props.dispatcher}
|
||||
emoji={this.state.emoji}
|
||||
sidebarWidth={this.state.sidebarWidth}
|
||||
commitSummaryWidth={this.state.commitSummaryWidth}
|
||||
emoji={state.emoji}
|
||||
sidebarWidth={state.sidebarWidth}
|
||||
commitSummaryWidth={state.commitSummaryWidth}
|
||||
issuesStore={this.props.appStore.issuesStore}
|
||||
gitHubUserStore={this.props.appStore.gitHubUserStore}
|
||||
onViewCommitOnGitHub={this.onViewCommitOnGitHub}
|
||||
imageDiffType={this.state.imageDiffType}
|
||||
imageDiffType={state.imageDiffType}
|
||||
askForConfirmationOnDiscardChanges={
|
||||
this.state.askForConfirmationOnDiscardChanges
|
||||
state.askForConfirmationOnDiscardChanges
|
||||
}
|
||||
accounts={this.state.accounts}
|
||||
accounts={state.accounts}
|
||||
externalEditorLabel={externalEditorLabel}
|
||||
onOpenInExternalEditor={this.openFileInExternalEditor}
|
||||
isDivergingBranchBannerVisible={state.isDivergingBranchBannerVisible}
|
||||
/>
|
||||
)
|
||||
} else if (selectedState.type === SelectionType.CloningRepository) {
|
||||
|
@ -1706,8 +1713,13 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
|
||||
const className = this.state.appIsFocused ? 'focused' : 'blurred'
|
||||
|
||||
const currentTheme = this.state.showWelcomeFlow
|
||||
? ApplicationTheme.Light
|
||||
: this.state.selectedTheme
|
||||
|
||||
return (
|
||||
<div id="desktop-app-chrome" className={className}>
|
||||
<AppTheme theme={currentTheme} />
|
||||
{this.renderTitlebar()}
|
||||
{this.state.showWelcomeFlow
|
||||
? this.renderWelcomeFlow()
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import * as React from 'react'
|
||||
import { CSSTransitionGroup } from 'react-transition-group'
|
||||
|
||||
import { IGitHubUser } from '../../lib/databases'
|
||||
import { Commit } from '../../models/commit'
|
||||
import {
|
||||
|
@ -22,7 +24,8 @@ import { OcticonSymbol } from '../octicons'
|
|||
import { SelectionSource } from '../lib/filter-list'
|
||||
import { IMatches } from '../../lib/fuzzy-find'
|
||||
import { Ref } from '../lib/ref'
|
||||
|
||||
import { NewCommitsBanner } from '../notification/new-commits-banner'
|
||||
import { enableNotificationOfBranchUpdates } from '../../lib/feature-flag'
|
||||
import { MergeCallToAction } from './merge-call-to-action'
|
||||
|
||||
interface ICompareSidebarProps {
|
||||
|
@ -34,6 +37,7 @@ interface ICompareSidebarProps {
|
|||
readonly localCommitSHAs: ReadonlyArray<string>
|
||||
readonly dispatcher: Dispatcher
|
||||
readonly currentBranch: Branch | null
|
||||
readonly isDivergingBranchBannerVisible: boolean
|
||||
readonly onRevertCommit: (commit: Commit) => void
|
||||
readonly onViewCommitOnGitHub: (sha: string) => void
|
||||
}
|
||||
|
@ -58,6 +62,7 @@ export class CompareSidebar extends React.Component<
|
|||
private textbox: TextBox | null = null
|
||||
private readonly loadChangedFilesScheduler = new ThrottledScheduler(200)
|
||||
private branchList: BranchList | null = null
|
||||
private loadingMoreCommitsPromise: Promise<void> | null = null
|
||||
|
||||
public constructor(props: ICompareSidebarProps) {
|
||||
super(props)
|
||||
|
@ -127,9 +132,20 @@ export class CompareSidebar extends React.Component<
|
|||
public render() {
|
||||
const { allBranches, filterText, showBranchList } = this.props.compareState
|
||||
const placeholderText = getPlaceholderText(this.props.compareState)
|
||||
const DivergingBannerAnimationTimeout = 300
|
||||
|
||||
return (
|
||||
<div id="compare-view">
|
||||
<CSSTransitionGroup
|
||||
transitionName="diverge-banner"
|
||||
transitionAppear={true}
|
||||
transitionAppearTimeout={DivergingBannerAnimationTimeout}
|
||||
transitionEnterTimeout={DivergingBannerAnimationTimeout}
|
||||
transitionLeaveTimeout={DivergingBannerAnimationTimeout}
|
||||
>
|
||||
{this.renderNotificationBanner()}
|
||||
</CSSTransitionGroup>
|
||||
|
||||
<div className="compare-form">
|
||||
<FancyTextBox
|
||||
symbol={OcticonSymbol.gitBranch}
|
||||
|
@ -137,7 +153,7 @@ export class CompareSidebar extends React.Component<
|
|||
placeholder={placeholderText}
|
||||
onFocus={this.onTextBoxFocused}
|
||||
value={filterText}
|
||||
disabled={allBranches.length <= 1}
|
||||
disabled={allBranches.length === 0}
|
||||
onRef={this.onTextBoxRef}
|
||||
onValueChanged={this.onBranchFilterTextChanged}
|
||||
onKeyDown={this.onBranchFilterKeyDown}
|
||||
|
@ -154,6 +170,28 @@ export class CompareSidebar extends React.Component<
|
|||
this.branchList = branchList
|
||||
}
|
||||
|
||||
private renderNotificationBanner() {
|
||||
if (
|
||||
!enableNotificationOfBranchUpdates ||
|
||||
!this.props.isDivergingBranchBannerVisible
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { inferredComparisonBranch } = this.props.compareState
|
||||
|
||||
return inferredComparisonBranch.branch !== null &&
|
||||
inferredComparisonBranch.aheadBehind !== null ? (
|
||||
<div className="diverge-banner-wrapper">
|
||||
<NewCommitsBanner
|
||||
commitsBehindBaseBranch={inferredComparisonBranch.aheadBehind.behind}
|
||||
baseBranch={inferredComparisonBranch.branch}
|
||||
onDismiss={this.onNotificationBannerDismissed}
|
||||
/>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
private renderCommits() {
|
||||
const formState = this.props.compareState.formState
|
||||
return (
|
||||
|
@ -401,7 +439,22 @@ export class CompareSidebar extends React.Component<
|
|||
|
||||
const commits = compareState.commitSHAs
|
||||
if (commits.length - end <= CloseToBottomThreshold) {
|
||||
this.props.dispatcher.loadNextHistoryBatch(this.props.repository)
|
||||
if (this.loadingMoreCommitsPromise != null) {
|
||||
// as this callback fires for any scroll event we need to guard
|
||||
// against re-entrant calls to loadNextHistoryBatch
|
||||
return
|
||||
}
|
||||
|
||||
this.loadingMoreCommitsPromise = this.props.dispatcher
|
||||
.loadNextHistoryBatch(this.props.repository)
|
||||
.then(() => {
|
||||
// deferring unsetting this flag to some time _after_ the commits
|
||||
// have been appended to prevent eagerly adding more commits due
|
||||
// to scroll events (which fire indiscriminately)
|
||||
window.setTimeout(() => {
|
||||
this.loadingMoreCommitsPromise = null
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -470,12 +523,17 @@ export class CompareSidebar extends React.Component<
|
|||
private onTextBoxRef = (textbox: TextBox) => {
|
||||
this.textbox = textbox
|
||||
}
|
||||
|
||||
private onNotificationBannerDismissed = () => {
|
||||
this.props.dispatcher.setDivergingBranchBannerVisibility(false)
|
||||
this.props.dispatcher.recordDivergingBranchBannerDismissal()
|
||||
}
|
||||
}
|
||||
|
||||
function getPlaceholderText(state: ICompareState) {
|
||||
const { allBranches, formState } = state
|
||||
|
||||
if (allBranches.length <= 1) {
|
||||
if (allBranches.length === 0) {
|
||||
return __DARWIN__ ? 'No Branches to Compare' : 'No branches to compare'
|
||||
} else if (formState.kind === ComparisonView.None) {
|
||||
return __DARWIN__
|
||||
|
|
|
@ -151,10 +151,6 @@ dispatcher.registerErrorHandler(missingRepositoryHandler)
|
|||
|
||||
document.body.classList.add(`platform-${process.platform}`)
|
||||
|
||||
if (localStorage.getItem('theme') === 'dark') {
|
||||
document.body.classList.add(`theme-dark`)
|
||||
}
|
||||
|
||||
dispatcher.setAppFocusState(remote.getCurrentWindow().isFocused())
|
||||
|
||||
ipcRenderer.on('focus', () => {
|
||||
|
|
56
app/src/ui/lib/application-theme.ts
Normal file
56
app/src/ui/lib/application-theme.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { assertNever } from '../../lib/fatal-error'
|
||||
|
||||
/**
|
||||
* A set of the user-selectable appearances (aka themes)
|
||||
*/
|
||||
export enum ApplicationTheme {
|
||||
Light,
|
||||
Dark,
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the friendly name of an application theme for use
|
||||
* in persisting to storage and/or calculating the required
|
||||
* body class name to set in order to apply the theme.
|
||||
*/
|
||||
export function getThemeName(theme: ApplicationTheme): string {
|
||||
switch (theme) {
|
||||
case ApplicationTheme.Light:
|
||||
return 'light'
|
||||
case ApplicationTheme.Dark:
|
||||
return 'dark'
|
||||
default:
|
||||
return assertNever(theme, `Unknown theme ${theme}`)
|
||||
}
|
||||
}
|
||||
|
||||
// The key under which the currently selected theme is persisted
|
||||
// in localStorage.
|
||||
const applicationThemeKey = 'theme'
|
||||
|
||||
/**
|
||||
* Load the currently selected theme from the persistent
|
||||
* store (localStorage). If no theme is selected the default
|
||||
* theme will be returned.
|
||||
*/
|
||||
export function getPersistedTheme(): ApplicationTheme {
|
||||
return localStorage.getItem(applicationThemeKey) === 'dark'
|
||||
? ApplicationTheme.Dark
|
||||
: ApplicationTheme.Light
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the name of the currently selected theme from the persistent
|
||||
* store (localStorage). If no theme is selected the default
|
||||
* theme name will be returned.
|
||||
*/
|
||||
export function getPersistedThemeName(): string {
|
||||
return getThemeName(getPersistedTheme())
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the given theme in the persistent store (localStorage).
|
||||
*/
|
||||
export function setPersistedTheme(theme: ApplicationTheme) {
|
||||
localStorage.setItem(applicationThemeKey, getThemeName(theme))
|
||||
}
|
|
@ -14,6 +14,11 @@ interface INewCommitsBannerProps {
|
|||
* from the current branch
|
||||
*/
|
||||
readonly baseBranch: Branch
|
||||
|
||||
/**
|
||||
* Callback used to dismiss the banner
|
||||
*/
|
||||
readonly onDismiss: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -45,7 +50,11 @@ export class NewCommitsBanner extends React.Component<
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<a className="close" aria-label="Dismiss banner">
|
||||
<a
|
||||
className="close"
|
||||
aria-label="Dismiss banner"
|
||||
onClick={this.props.onDismiss}
|
||||
>
|
||||
<Octicon symbol={OcticonSymbol.x} />
|
||||
</a>
|
||||
</div>
|
||||
|
|
49
app/src/ui/preferences/appearance.tsx
Normal file
49
app/src/ui/preferences/appearance.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import * as React from 'react'
|
||||
import { DialogContent } from '../dialog'
|
||||
import {
|
||||
VerticalSegmentedControl,
|
||||
ISegmentedItem,
|
||||
} from '../lib/vertical-segmented-control'
|
||||
import { ApplicationTheme } from '../lib/application-theme'
|
||||
import { fatalError } from '../../lib/fatal-error'
|
||||
|
||||
interface IAppearanceProps {
|
||||
readonly selectedTheme: ApplicationTheme
|
||||
readonly onSelectedThemeChanged: (theme: ApplicationTheme) => void
|
||||
}
|
||||
|
||||
const themes: ReadonlyArray<ISegmentedItem> = [
|
||||
{ title: 'Light', description: 'The default theme of GitHub Desktop' },
|
||||
{
|
||||
title: 'Dark (beta)',
|
||||
description:
|
||||
'A beta version of our dark theme. Still under development. Please report any issues you may find to our issue tracker.',
|
||||
},
|
||||
]
|
||||
|
||||
export class Appearance extends React.Component<IAppearanceProps, {}> {
|
||||
private onSelectedThemeChanged = (index: number) => {
|
||||
if (index === 0) {
|
||||
this.props.onSelectedThemeChanged(ApplicationTheme.Light)
|
||||
} else if (index === 1) {
|
||||
this.props.onSelectedThemeChanged(ApplicationTheme.Dark)
|
||||
} else {
|
||||
fatalError(`Unknown theme index ${index}`)
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const selectedIndex =
|
||||
this.props.selectedTheme === ApplicationTheme.Dark ? 1 : 0
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<VerticalSegmentedControl
|
||||
items={themes}
|
||||
selectedIndex={selectedIndex}
|
||||
onSelectionChanged={this.onSelectedThemeChanged}
|
||||
/>
|
||||
</DialogContent>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -21,6 +21,8 @@ import { lookupPreferredEmail } from '../../lib/email'
|
|||
import { Shell, getAvailableShells } from '../../lib/shells'
|
||||
import { getAvailableEditors } from '../../lib/editors/lookup'
|
||||
import { disallowedCharacters } from './identifier-rules'
|
||||
import { Appearance } from './appearance'
|
||||
import { ApplicationTheme } from '../lib/application-theme'
|
||||
|
||||
interface IPreferencesProps {
|
||||
readonly dispatcher: Dispatcher
|
||||
|
@ -33,6 +35,7 @@ interface IPreferencesProps {
|
|||
readonly confirmDiscardChanges: boolean
|
||||
readonly selectedExternalEditor?: ExternalEditor
|
||||
readonly selectedShell: Shell
|
||||
readonly selectedTheme: ApplicationTheme
|
||||
}
|
||||
|
||||
interface IPreferencesState {
|
||||
|
@ -134,6 +137,7 @@ export class Preferences extends React.Component<
|
|||
>
|
||||
<span>Accounts</span>
|
||||
<span>Git</span>
|
||||
<span>Appearance</span>
|
||||
<span>Advanced</span>
|
||||
</TabBar>
|
||||
|
||||
|
@ -203,6 +207,13 @@ export class Preferences extends React.Component<
|
|||
/>
|
||||
)
|
||||
}
|
||||
case PreferencesTab.Appearance:
|
||||
return (
|
||||
<Appearance
|
||||
selectedTheme={this.props.selectedTheme}
|
||||
onSelectedThemeChanged={this.onSelectedThemeChanged}
|
||||
/>
|
||||
)
|
||||
case PreferencesTab.Advanced: {
|
||||
return (
|
||||
<Advanced
|
||||
|
@ -269,12 +280,17 @@ export class Preferences extends React.Component<
|
|||
this.setState({ selectedShell: shell })
|
||||
}
|
||||
|
||||
private onSelectedThemeChanged = (theme: ApplicationTheme) => {
|
||||
this.props.dispatcher.setSelectedTheme(theme)
|
||||
}
|
||||
|
||||
private renderFooter() {
|
||||
const hasDisabledError = this.state.disallowedCharactersMessage != null
|
||||
|
||||
const index = this.state.selectedIndex
|
||||
switch (index) {
|
||||
case PreferencesTab.Accounts:
|
||||
case PreferencesTab.Appearance:
|
||||
return null
|
||||
case PreferencesTab.Advanced:
|
||||
case PreferencesTab.Git: {
|
||||
|
|
|
@ -22,6 +22,9 @@ interface IRepositoriesListProps {
|
|||
/** Called when a repository has been selected. */
|
||||
readonly onSelectionChanged: (repository: Repositoryish) => void
|
||||
|
||||
/** Whether the user has enabled the setting to confirm removing a repository from the app */
|
||||
readonly askForConfirmationOnRemoveRepository: boolean
|
||||
|
||||
/** Called when the repository should be removed. */
|
||||
readonly onRemoveRepository: (repository: Repositoryish) => void
|
||||
|
||||
|
@ -61,6 +64,9 @@ export class RepositoriesList extends React.Component<
|
|||
key={repository.id}
|
||||
repository={repository}
|
||||
needsDisambiguation={item.needsDisambiguation}
|
||||
askForConfirmationOnRemoveRepository={
|
||||
this.props.askForConfirmationOnRemoveRepository
|
||||
}
|
||||
onRemoveRepository={this.props.onRemoveRepository}
|
||||
onShowRepository={this.props.onShowRepository}
|
||||
onOpenInShell={this.props.onOpenInShell}
|
||||
|
|
|
@ -14,6 +14,9 @@ const defaultEditorLabel = __DARWIN__
|
|||
interface IRepositoryListItemProps {
|
||||
readonly repository: Repositoryish
|
||||
|
||||
/** Whether the user has enabled the setting to confirm removing a repository from the app */
|
||||
readonly askForConfirmationOnRemoveRepository: boolean
|
||||
|
||||
/** Called when the repository should be removed. */
|
||||
readonly onRemoveRepository: (repository: Repositoryish) => void
|
||||
|
||||
|
@ -124,7 +127,9 @@ export class RepositoryListItem extends React.Component<
|
|||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Remove',
|
||||
label: this.props.askForConfirmationOnRemoveRepository
|
||||
? 'Remove…'
|
||||
: 'Remove',
|
||||
action: this.removeRepository,
|
||||
},
|
||||
]
|
||||
|
|
|
@ -18,13 +18,10 @@ import {
|
|||
import { Dispatcher } from '../lib/dispatcher'
|
||||
import { IssuesStore, GitHubUserStore } from '../lib/stores'
|
||||
import { assertNever } from '../lib/fatal-error'
|
||||
import { Octicon, OcticonSymbol } from './octicons'
|
||||
import { Account } from '../models/account'
|
||||
import {
|
||||
enableCompareSidebar,
|
||||
enableNotificationOfBranchUpdates,
|
||||
} from '../lib/feature-flag'
|
||||
import { enableCompareSidebar } from '../lib/feature-flag'
|
||||
import { FocusContainer } from './lib/focus-container'
|
||||
import { OcticonSymbol, Octicon } from './octicons'
|
||||
|
||||
/** The widest the sidebar can be with the minimum window size. */
|
||||
const MaxSidebarWidth = 495
|
||||
|
@ -51,8 +48,13 @@ interface IRepositoryViewProps {
|
|||
*
|
||||
* @param fullPath The full path to the file on disk
|
||||
*/
|
||||
|
||||
readonly onOpenInExternalEditor: (fullPath: string) => void
|
||||
|
||||
/**
|
||||
* Determines if the notification banner and associated dot
|
||||
* on this history tab will be rendered
|
||||
*/
|
||||
readonly isDivergingBranchBannerVisible: boolean
|
||||
}
|
||||
|
||||
interface IRepositoryViewState {
|
||||
|
@ -84,11 +86,7 @@ export class RepositoryView extends React.Component<
|
|||
return null
|
||||
}
|
||||
|
||||
return enableNotificationOfBranchUpdates() ? (
|
||||
<FilesChangedBadge filesChangedCount={filesChangedCount} />
|
||||
) : (
|
||||
<Octicon className="indicator" symbol={OcticonSymbol.primitiveDot} />
|
||||
)
|
||||
return <FilesChangedBadge filesChangedCount={filesChangedCount} />
|
||||
}
|
||||
|
||||
private renderTabs(): JSX.Element {
|
||||
|
@ -103,7 +101,16 @@ export class RepositoryView extends React.Component<
|
|||
<span>Changes</span>
|
||||
{this.renderChangesBadge()}
|
||||
</span>
|
||||
<span>History</span>
|
||||
|
||||
<div className="with-indicator">
|
||||
<span>History</span>
|
||||
{this.props.isDivergingBranchBannerVisible ? (
|
||||
<Octicon
|
||||
className="indicator"
|
||||
symbol={OcticonSymbol.primitiveDot}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</TabBar>
|
||||
)
|
||||
}
|
||||
|
@ -180,6 +187,9 @@ export class RepositoryView extends React.Component<
|
|||
dispatcher={this.props.dispatcher}
|
||||
onRevertCommit={this.onRevertCommit}
|
||||
onViewCommitOnGitHub={this.props.onViewCommitOnGitHub}
|
||||
isDivergingBranchBannerVisible={
|
||||
this.props.isDivergingBranchBannerVisible
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -25,7 +25,10 @@ export class UpdateAvailable extends React.Component<
|
|||
An updated version of GitHub Desktop is available and will be
|
||||
installed at the next launch. See{' '}
|
||||
<LinkButton uri={this.props.releaseNotesLink}>what's new</LinkButton>{' '}
|
||||
or <LinkButton onClick={this.updateNow}>restart now</LinkButton>.
|
||||
or{' '}
|
||||
<LinkButton onClick={this.updateNow}>
|
||||
restart GitHub Desktop
|
||||
</LinkButton>.
|
||||
</span>
|
||||
|
||||
<a className="close" onClick={this.dismiss}>
|
||||
|
|
|
@ -42,11 +42,8 @@ $overlay-background-color: rgba(0, 0, 0, 0.4);
|
|||
// Typography
|
||||
//
|
||||
// Font, line-height, and color for body text, headings, and more.
|
||||
--font-family-sans-serif: -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||||
'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
|
||||
'Helvetica Neue', Arial, sans-serif;
|
||||
--font-family-monospace: Menlo, Monaco, Consolas, 'Liberation Mono',
|
||||
'Courier New', monospace;
|
||||
--font-family-sans-serif: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', Arial, sans-serif;
|
||||
--font-family-monospace: Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||
|
||||
/**
|
||||
* Font weight to use for semibold text
|
||||
|
@ -99,12 +96,7 @@ $overlay-background-color: rgba(0, 0, 0, 0.4);
|
|||
* Background color for skeleton or "loading" boxes
|
||||
*/
|
||||
--box-skeleton-background-color: $gray-200;
|
||||
--skeleton-background-gradient: -webkit-linear-gradient(
|
||||
left,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 0.5) 50%,
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
--skeleton-background-gradient: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.5) 50%, rgba(255, 255, 255, 0) 100%);
|
||||
|
||||
/**
|
||||
* Border color for boxes.
|
||||
|
@ -162,12 +154,13 @@ $overlay-background-color: rgba(0, 0, 0, 0.4);
|
|||
* for use when content can be expanded through other means than
|
||||
* scrolling.
|
||||
*/
|
||||
--box-overflow-shadow-background: linear-gradient(
|
||||
180deg,
|
||||
rgba($white, 0) 0%,
|
||||
rgba($white, 1) 90%,
|
||||
rgba($white, 1) 100%
|
||||
);
|
||||
--box-overflow-shadow-background: linear-gradient(180deg, rgba($white, 0) 0%, rgba($white, 1) 90%, rgba($white, 1) 100%);
|
||||
|
||||
/**
|
||||
* Author input (co-authors)
|
||||
*/
|
||||
--co-author-tag-background-color: $blue-000;
|
||||
--co-author-tag-border-color: $blue-200;
|
||||
|
||||
/**
|
||||
* The height of the title bar area on Win32 platforms
|
||||
|
@ -222,6 +215,19 @@ $overlay-background-color: rgba(0, 0, 0, 0.4);
|
|||
--toolbar-button-hover-progress-color: $gray-700;
|
||||
--toolbar-dropdown-open-progress-color: $gray-200;
|
||||
|
||||
/**
|
||||
* App menu bar colors (Windows/Linux only)
|
||||
*/
|
||||
--app-menu-button-color: var(--toolbar-text-color);
|
||||
--app-menu-button-hover-color: var(--toolbar-button-hover-color);
|
||||
--app-menu-button-hover-background-color: var(--toolbar-button-hover-background-color);
|
||||
--app-menu-button-active-color: var(--toolbar-button-active-color);
|
||||
--app-menu-button-active-background-color: var(--toolbar-button-active-background-color);
|
||||
--app-menu-pane-color: var(--text-color);
|
||||
--app-menu-pane-secondary-color: var(--text-secondary-color);
|
||||
--app-menu-pane-background-color: var(--toolbar-button-active-background-color);
|
||||
--app-menu-divider-color: var(--box-border-color);
|
||||
|
||||
/**
|
||||
* Background color for badges inside of toolbar buttons.
|
||||
* Examples of badges are the ahead/behind bubble in the
|
||||
|
|
|
@ -60,12 +60,7 @@ body.theme-dark {
|
|||
* Background color for skeleton or "loading" boxes
|
||||
*/
|
||||
--box-skeleton-background-color: $gray-700;
|
||||
--skeleton-background-gradient: -webkit-linear-gradient(
|
||||
left,
|
||||
rgba(36, 41, 46, 0) 0%,
|
||||
rgba(36, 41, 46, 0.5) 50%,
|
||||
rgba(36, 41, 46, 0) 100%
|
||||
);
|
||||
--skeleton-background-gradient: -webkit-linear-gradient(left, rgba(36, 41, 46, 0) 0%, rgba(36, 41, 46, 0.5) 50%, rgba(36, 41, 46, 0) 100%);
|
||||
|
||||
/**
|
||||
* Border color for boxes.
|
||||
|
@ -123,12 +118,13 @@ body.theme-dark {
|
|||
* for use when content can be expanded through other means than
|
||||
* scrolling.
|
||||
*/
|
||||
--box-overflow-shadow-background: linear-gradient(
|
||||
180deg,
|
||||
rgba($gray-900, 0) 0%,
|
||||
rgba($gray-900, 1) 90%,
|
||||
rgba($gray-900, 1) 100%
|
||||
);
|
||||
--box-overflow-shadow-background: linear-gradient(180deg, rgba($gray-900, 0) 0%, rgba($gray-900, 1) 90%, rgba($gray-900, 1) 100%);
|
||||
|
||||
/**
|
||||
* Author input (co-authors)
|
||||
*/
|
||||
--co-author-tag-background-color: $blue-800;
|
||||
--co-author-tag-border-color: $blue-700;
|
||||
|
||||
--base-border: 1px solid var(--box-border-color);
|
||||
|
||||
|
@ -160,6 +156,19 @@ body.theme-dark {
|
|||
--toolbar-button-hover-progress-color: $gray-700;
|
||||
--toolbar-dropdown-open-progress-color: $gray-200;
|
||||
|
||||
/**
|
||||
* App menu bar colors (Windows/Linux only)
|
||||
*/
|
||||
--app-menu-button-color: var(--toolbar-text-color);
|
||||
--app-menu-button-hover-color: var(--toolbar-button-hover-color);
|
||||
--app-menu-button-hover-background-color: var(--toolbar-button-hover-background-color);
|
||||
--app-menu-button-active-color: var(--text-color);
|
||||
--app-menu-button-active-background-color: $gray-800;
|
||||
--app-menu-pane-color: var(--text-color);
|
||||
--app-menu-pane-secondary-color: var(--text-secondary-color);
|
||||
--app-menu-pane-background-color: $gray-800;
|
||||
--app-menu-divider-color: $gray-600;
|
||||
|
||||
/**
|
||||
* Background color for badges inside of toolbar buttons.
|
||||
* Examples of badges are the ahead/behind bubble in the
|
||||
|
@ -276,4 +285,9 @@ body.theme-dark {
|
|||
.blankslate-image {
|
||||
filter: #{'invert()'} grayscale(1) brightness(8) contrast(0.6);
|
||||
}
|
||||
|
||||
/** Diverging notification banner colors */
|
||||
--notification-banner-background: $gray-800;
|
||||
--notification-banner-border-color: $gray-700;
|
||||
--notification-ref-background: $gray-700;
|
||||
}
|
||||
|
|
|
@ -116,6 +116,6 @@
|
|||
width: 100%;
|
||||
border: none;
|
||||
height: 1px;
|
||||
border-bottom: var(--base-border);
|
||||
border-bottom: 1px solid var(--app-menu-divider-color);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,8 +24,8 @@
|
|||
|
||||
.handle {
|
||||
border-radius: 3px;
|
||||
border: 1px solid #c3e1ff;
|
||||
background: #f0f8ff;
|
||||
border: 1px solid var(--co-author-tag-border-color);
|
||||
background: var(--co-author-tag-background-color);
|
||||
padding: 1px 1px;
|
||||
margin: 0px 2px;
|
||||
|
||||
|
|
|
@ -71,8 +71,7 @@ dialog {
|
|||
&-leave-active {
|
||||
opacity: 0.01;
|
||||
transform: scale(0.25);
|
||||
transition: opacity 100ms ease-in,
|
||||
transform 100ms var(--easing-ease-in-back);
|
||||
transition: opacity 100ms ease-in, transform 100ms var(--easing-ease-in-back);
|
||||
|
||||
&::backdrop {
|
||||
opacity: 0.01;
|
||||
|
@ -136,10 +135,7 @@ dialog {
|
|||
// Ensure that the dialog contents always have room for the icon,
|
||||
// account for two double spacers at top and bottom plus the 5px
|
||||
// icon offset (margin-top) and the size of the icon itself.
|
||||
min-height: calc(
|
||||
var(--spacing-double) * 2 + var(--spacing-half) +
|
||||
var(--dialog-icon-size)
|
||||
);
|
||||
min-height: calc(var(--spacing-double) * 2 + var(--spacing-half) + var(--dialog-icon-size));
|
||||
|
||||
// We're creating an opaque 24 by 24px div with the background color
|
||||
// that we want the icon to appear in and then apply the icon path
|
||||
|
|
|
@ -56,9 +56,36 @@
|
|||
margin: var(--spacing-half);
|
||||
overflow: hidden;
|
||||
|
||||
&-enter {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
|
||||
&-active {
|
||||
max-height: 200px;
|
||||
opacity: 1;
|
||||
transition: all var(--undo-animation-duration) cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
&-leave {
|
||||
max-height: 200px;
|
||||
|
||||
&-active {
|
||||
max-height: 0;
|
||||
opacity: 1;
|
||||
transition: max-height 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
.diverge-banner {
|
||||
transform: translateX(-102%);
|
||||
opacity: 0;
|
||||
transition: all 180ms cubic-bezier(0.4, 0, 1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-wrapper {
|
||||
overflow: hidden;
|
||||
border-bottom: var(--base-border);
|
||||
box-shadow: inset 0 -1px var(--box-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,13 +12,7 @@ progress {
|
|||
}
|
||||
|
||||
&:indeterminate {
|
||||
background-image: -webkit-linear-gradient(
|
||||
-45deg,
|
||||
transparent 33%,
|
||||
var(--text-color) 33%,
|
||||
var(--text-color) 66%,
|
||||
transparent 66%
|
||||
);
|
||||
background-image: -webkit-linear-gradient(-45deg, transparent 33%, var(--text-color) 33%, var(--text-color) 66%, transparent 66%);
|
||||
background-size: 25px 10px, 100% 100%, 100% 100%;
|
||||
|
||||
-webkit-animation: progress-indeterminate-animation 5s linear infinite;
|
||||
|
|
|
@ -91,11 +91,7 @@
|
|||
width: 100%;
|
||||
bottom: 0px;
|
||||
height: 5px;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 0.1) 100%
|
||||
);
|
||||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.1) 100%);
|
||||
border-bottom: var(--base-border);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,12 +10,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
.toolbar-dropdown.open > .toolbar-button > button {
|
||||
background-color: var(--app-menu-button-active-background-color);
|
||||
color: var(--app-menu-button-active-color);
|
||||
}
|
||||
|
||||
.toolbar-dropdown:not(.open) > .toolbar-button > button {
|
||||
color: var(--toolbar-text-color);
|
||||
color: var(--app-menu-button-color);
|
||||
background: transparent;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--toolbar-button-color);
|
||||
color: var(--app-menu-button-hover-color);
|
||||
background: var(--app-menu-button-hover-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,7 +46,14 @@
|
|||
pointer-events: none;
|
||||
|
||||
.menu-pane {
|
||||
--background-color: var(--app-menu-pane-background-color);
|
||||
background: var(--background-color);
|
||||
|
||||
--text-color: var(--app-menu-pane-color);
|
||||
color: var(--text-color);
|
||||
|
||||
--text-secondary-color: var(--app-menu-pane-secondary-color);
|
||||
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
|
|
205
app/test/unit/infer-comparison-branch-test.ts
Normal file
205
app/test/unit/infer-comparison-branch-test.ts
Normal file
|
@ -0,0 +1,205 @@
|
|||
import { expect } from 'chai'
|
||||
import { inferComparisonBranch } from '../../src/lib/stores/helpers/infer-comparison-branch'
|
||||
import { Branch, BranchType } from '../../src/models/branch'
|
||||
import { Commit } from '../../src/models/commit'
|
||||
import { CommitIdentity } from '../../src/models/commit-identity'
|
||||
import { GitHubRepository } from '../../src/models/github-repository'
|
||||
import { Owner } from '../../src/models/owner'
|
||||
import { PullRequest, PullRequestRef } from '../../src/models/pull-request'
|
||||
import { Repository } from '../../src/models/repository'
|
||||
import { IRemote } from '../../src/models/remote'
|
||||
import { ComparisonCache } from '../../src/lib/comparison-cache'
|
||||
|
||||
function createTestCommit(sha: string) {
|
||||
return new Commit(
|
||||
sha,
|
||||
'',
|
||||
'',
|
||||
new CommitIdentity('tester', 'tester@test.com', new Date()),
|
||||
new CommitIdentity('tester', 'tester@test.com', new Date()),
|
||||
[],
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
function createTestBranch(
|
||||
name: string,
|
||||
sha: string,
|
||||
remote: string | null = null
|
||||
) {
|
||||
return new Branch(name, remote, createTestCommit(sha), BranchType.Local)
|
||||
}
|
||||
|
||||
function createTestGhRepo(
|
||||
name: string,
|
||||
defaultBranch: string | null = null,
|
||||
parent: GitHubRepository | null = null
|
||||
) {
|
||||
return new GitHubRepository(
|
||||
name,
|
||||
new Owner('', '', null),
|
||||
null,
|
||||
false,
|
||||
'',
|
||||
`${
|
||||
defaultBranch !== null && defaultBranch.indexOf('/') !== -1
|
||||
? defaultBranch.split('/')[1]
|
||||
: defaultBranch
|
||||
}`,
|
||||
`${name.indexOf('/') !== -1 ? name.split('/')[1] : name}.git`,
|
||||
parent
|
||||
)
|
||||
}
|
||||
|
||||
function createTestPrRef(
|
||||
branch: Branch,
|
||||
ghRepo: GitHubRepository | null = null
|
||||
) {
|
||||
return new PullRequestRef(branch.name, branch.tip.sha, ghRepo)
|
||||
}
|
||||
|
||||
function createTestPr(head: PullRequestRef, base: PullRequestRef) {
|
||||
return new PullRequest(-1, new Date(), null, '', 1, head, base, '')
|
||||
}
|
||||
|
||||
function createTestRepo(ghRepo: GitHubRepository | null = null) {
|
||||
return new Repository('', -1, ghRepo, false)
|
||||
}
|
||||
|
||||
function mockGetRemotes(repo: Repository): Promise<ReadonlyArray<IRemote>> {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
|
||||
describe('inferComparisonBranch', () => {
|
||||
const branches = [
|
||||
createTestBranch('master', '0', 'origin'),
|
||||
createTestBranch('dev', '1', 'origin'),
|
||||
createTestBranch('staging', '2', 'origin'),
|
||||
createTestBranch('default', '3', 'origin'),
|
||||
createTestBranch('head', '4', 'origin'),
|
||||
createTestBranch('upstream/base', '5', 'upstream'),
|
||||
createTestBranch('fork', '6', 'origin'),
|
||||
]
|
||||
const comparisonCache = new ComparisonCache()
|
||||
|
||||
beforeEach(() => {
|
||||
comparisonCache.clear()
|
||||
})
|
||||
|
||||
it('Returns the master branch when given unhosted repo', async () => {
|
||||
const repo = createTestRepo()
|
||||
const branch = await inferComparisonBranch(
|
||||
repo,
|
||||
branches,
|
||||
null,
|
||||
null,
|
||||
mockGetRemotes,
|
||||
comparisonCache
|
||||
)
|
||||
|
||||
expect(branch).is.not.null
|
||||
expect(branch!.tip.sha).to.equal('0')
|
||||
})
|
||||
|
||||
it('Returns the default branch of a GitHub repository', async () => {
|
||||
const ghRepo: GitHubRepository = createTestGhRepo('test', 'default')
|
||||
const repo = createTestRepo(ghRepo)
|
||||
|
||||
const branch = await inferComparisonBranch(
|
||||
repo,
|
||||
branches,
|
||||
null,
|
||||
null,
|
||||
mockGetRemotes,
|
||||
comparisonCache
|
||||
)
|
||||
|
||||
expect(branch).is.not.null
|
||||
expect(branch!.name).to.equal('default')
|
||||
})
|
||||
|
||||
it('Returns the branch associated with the PR', async () => {
|
||||
const ghRepo: GitHubRepository = createTestGhRepo('test', 'default')
|
||||
const repo = createTestRepo(ghRepo)
|
||||
const head = createTestPrRef(branches[4])
|
||||
const base = createTestPrRef(branches[5])
|
||||
const pr: PullRequest = createTestPr(head, base)
|
||||
|
||||
const branch = await inferComparisonBranch(
|
||||
repo,
|
||||
branches,
|
||||
pr,
|
||||
null,
|
||||
mockGetRemotes,
|
||||
comparisonCache
|
||||
)
|
||||
|
||||
expect(branch).is.not.null
|
||||
expect(branch!.upstream).to.equal(branches[5].upstream)
|
||||
})
|
||||
|
||||
it('Returns the default branch of the fork if it is ahead of the current branch', async () => {
|
||||
const currentBranch = branches[3]
|
||||
const defaultBranch = branches[6]
|
||||
const parent = createTestGhRepo('parent', 'parent')
|
||||
const fork = createTestGhRepo('fork', 'fork', parent)
|
||||
const repo = createTestRepo(fork)
|
||||
|
||||
comparisonCache.set(currentBranch.tip.sha, defaultBranch.tip.sha, {
|
||||
ahead: 1,
|
||||
behind: 0,
|
||||
})
|
||||
|
||||
const branch = await inferComparisonBranch(
|
||||
repo,
|
||||
branches,
|
||||
null,
|
||||
currentBranch,
|
||||
mockGetRemotes,
|
||||
comparisonCache
|
||||
)
|
||||
|
||||
expect(branch).is.not.null
|
||||
expect(branch!.name).to.equal(defaultBranch.name)
|
||||
})
|
||||
|
||||
it("Returns the default branch of the fork's parent branch if the fork is not ahead of the current branch", async () => {
|
||||
const defaultBranchOfParent = branches[5]
|
||||
const defaultBranchOfFork = branches[4]
|
||||
const parent = createTestGhRepo(
|
||||
'parent',
|
||||
defaultBranchOfParent.nameWithoutRemote
|
||||
)
|
||||
const fork = createTestGhRepo('fork', defaultBranchOfFork.name, parent)
|
||||
const repo = createTestRepo(fork)
|
||||
const mockGetRemotes = (repo: Repository) => {
|
||||
const remotes: ReadonlyArray<IRemote> = [
|
||||
{ name: 'origin', url: fork.cloneURL! },
|
||||
{ name: 'upstream', url: parent.cloneURL! },
|
||||
]
|
||||
|
||||
return Promise.resolve(remotes)
|
||||
}
|
||||
|
||||
comparisonCache.set(
|
||||
defaultBranchOfParent.tip.sha,
|
||||
defaultBranchOfFork.tip.sha,
|
||||
{
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
}
|
||||
)
|
||||
|
||||
const branch = await inferComparisonBranch(
|
||||
repo,
|
||||
branches,
|
||||
null,
|
||||
defaultBranchOfParent,
|
||||
mockGetRemotes,
|
||||
comparisonCache
|
||||
)
|
||||
|
||||
expect(branch).is.not.null
|
||||
expect(branch!.upstream).to.equal(defaultBranchOfParent.upstream)
|
||||
})
|
||||
})
|
|
@ -153,6 +153,7 @@ const highlighterConfig = merge({}, commonConfig, {
|
|||
chunkFilename: 'highlighter/[name].js',
|
||||
},
|
||||
optimization: {
|
||||
namedChunks: true,
|
||||
splitChunks: {
|
||||
cacheGroups: {
|
||||
modes: {
|
||||
|
|
|
@ -88,10 +88,6 @@ bcrypt-pbkdf@^1.0.0:
|
|||
dependencies:
|
||||
tweetnacl "^0.14.3"
|
||||
|
||||
big.js@^3.1.3:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.8"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
|
||||
|
@ -242,9 +238,9 @@ dom-matches@^2.0.0:
|
|||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dom-matches/-/dom-matches-2.0.0.tgz#d2728b416a87533980eb089b848d253cf23a758c"
|
||||
|
||||
dugite@^1.66.0:
|
||||
version "1.66.0"
|
||||
resolved "https://registry.yarnpkg.com/dugite/-/dugite-1.66.0.tgz#5fdab6683c0b538a79bdbec499e9f3d3ea7210f9"
|
||||
dugite@^1.67.0:
|
||||
version "1.67.0"
|
||||
resolved "https://registry.yarnpkg.com/dugite/-/dugite-1.67.0.tgz#389f95051aa1fb2bc78dcee4294f913ae3438c47"
|
||||
dependencies:
|
||||
checksum "^0.1.1"
|
||||
mkdirp "^0.5.1"
|
||||
|
@ -300,10 +296,6 @@ electron-window-state@^4.0.2:
|
|||
jsonfile "^2.2.3"
|
||||
mkdirp "^0.5.1"
|
||||
|
||||
emojis-list@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
|
||||
|
||||
encoding@^0.1.11:
|
||||
version "0.1.12"
|
||||
resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
|
||||
|
@ -517,10 +509,6 @@ json-stringify-safe@~5.0.1:
|
|||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
|
||||
|
||||
json5@^0.5.0:
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
|
||||
|
||||
jsonfile@^2.2.3:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8"
|
||||
|
@ -556,14 +544,6 @@ keytar@^4.0.4:
|
|||
dependencies:
|
||||
nan "2.5.1"
|
||||
|
||||
loader-utils@^1.0.2:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd"
|
||||
dependencies:
|
||||
big.js "^3.1.3"
|
||||
emojis-list "^2.0.0"
|
||||
json5 "^0.5.0"
|
||||
|
||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.0, loose-envify@^1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
|
||||
|
@ -888,12 +868,6 @@ strip-eof@^1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
|
||||
|
||||
style-loader@^0.13.2:
|
||||
version "0.13.2"
|
||||
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.13.2.tgz#74533384cf698c7104c7951150b49717adc2f3bb"
|
||||
dependencies:
|
||||
loader-utils "^1.0.2"
|
||||
|
||||
supports-color@^4.0.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e"
|
||||
|
|
|
@ -1,5 +1,49 @@
|
|||
{
|
||||
"releases": {
|
||||
"1.2.4": [
|
||||
"[New] Dark Theme preview - #4849",
|
||||
"[Added] Syntax highlighting for Cake files - #4935. Thanks @say25!",
|
||||
"[Added] WebStorm support for macOS - #4841. Thanks @mrsimonfletcher!",
|
||||
"[Fixed] Compare tab appends older commits when scrolling to bottom of list - #4964",
|
||||
"[Fixed] Remove temporary directory after Git LFS operation completes - #4414",
|
||||
"[Fixed] Unable to compare when two branches exist - #4947 #4730",
|
||||
"[Fixed] Unhandled errors when refreshing pull requests fails - #4844 #4866",
|
||||
"[Improved] Remove context menu needs to hint if a dialog will be shown - #4975",
|
||||
"[Improved] Upgrade embedded Git LFS - #4602 #4745",
|
||||
"[Improved] Update banner message clarifies that only Desktop needs to be restarted - #4891. Thanks @KennethSweezy!",
|
||||
"[Improved] Discard Changes context menu entry should contain ellipses when user needs to confirm - #4846. Thanks @yongdamsh!",
|
||||
"[Improved] Initializing syntax highlighting components - #4764",
|
||||
"[Improved] Only show overflow shadow when description overflows - #4898",
|
||||
"[Improved] Changes tab displays number of changed files instead of dot - #4772. Thanks @yongdamsh!"
|
||||
],
|
||||
"1.2.4-beta5": [
|
||||
|
||||
],
|
||||
"1.2.4-beta4": [
|
||||
"[Fixed] Compare tab appends older commits when scrolling to bottom of list - #4964",
|
||||
"[Fixed] Remove temporary directory after Git LFS operation completes - #4414",
|
||||
"[Improved] Remove context menu needs to hint if a dialog will be shown - #4975",
|
||||
"[Improved] Upgrade embedded Git LFS - #4602 #4745"
|
||||
],
|
||||
"1.2.4-test1": [
|
||||
"Confirming latest Git LFS version addresses reported issues"
|
||||
],
|
||||
"1.2.4-beta3": [
|
||||
"[Added] WebStorm support for macOS - #4841. Thanks @mrsimonfletcher!",
|
||||
"[Improved] Update banner message clarifies that only Desktop needs to be restarted - #4891. Thanks @KennethSweezy!"
|
||||
],
|
||||
"1.2.4-beta2": [
|
||||
],
|
||||
"1.2.4-beta1": [
|
||||
"[New] Dark Theme preview - #4849",
|
||||
"[Added] Syntax highlighting for Cake files - #4935. Thanks @say25!",
|
||||
"[Fixed] Unable to compare when two branches exist - #4947 #4730",
|
||||
"[Fixed] Unhandled errors when refreshing pull requests fails - #4844 #4866",
|
||||
"[Improved] Discard Changes context menu entry should contain ellipses when user needs to confirm - #4846. Thanks @yongdamsh!",
|
||||
"[Improved] Initializing syntax highlighting components - #4764",
|
||||
"[Improved] Only show overflow shadow when description overflows - #4898",
|
||||
"[Improved] Changes tab displays number of changed files instead of dot - #4772. Thanks @yongdamsh!"
|
||||
],
|
||||
"1.2.3": [
|
||||
"[Fixed] No autocomplete when searching for co-authors - #4847",
|
||||
"[Fixed] Error when checking out a PR from a fork - #4842"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# "Open External Editor" integration
|
||||
|
||||
GitHub Desktop supports the user choosing an external program to open their
|
||||
local repositories, and this is available from the top-level **Repository** menu
|
||||
local repositories, and this is available from the top-level **Repository** menu
|
||||
or when right-clicking on a repository in the sidebar.
|
||||
|
||||
### My favourite editor XYZ isn't supported!
|
||||
|
@ -212,10 +212,11 @@ These editors are currently supported:
|
|||
- [PhpStorm](https://www.jetbrains.com/phpstorm/)
|
||||
- [RubyMine](https://www.jetbrains.com/rubymine/)
|
||||
- [TextMate](https://macromates.com)
|
||||
- [Brackets](http://brackets.io/)
|
||||
- [Brackets](http://brackets.io/)
|
||||
- To use Brackets the Command Line shortcut must be installed.
|
||||
- This can be done by opening Brackets, choosing File > Install Command Line Shortcut
|
||||
|
||||
- [WebStorm](https://www.jetbrains.com/webstorm/)
|
||||
|
||||
These are defined in an enum at the top of the file:
|
||||
|
||||
```ts
|
||||
|
@ -231,6 +232,7 @@ export enum ExternalEditor {
|
|||
RubyMine = 'RubyMine',
|
||||
TextMate = 'TextMate',
|
||||
Brackets = 'Brackets',
|
||||
WebStorm = 'WebStorm',
|
||||
}
|
||||
```
|
||||
|
||||
|
|
Loading…
Reference in a new issue