Merge branch 'master' into bump-for-new-parser

This commit is contained in:
Brendan Forster 2018-06-25 12:19:13 -03:00
commit 7bd3f13b4a
41 changed files with 1115 additions and 132 deletions

View file

@ -2,3 +2,8 @@ singleQuote: true
trailingComma: es5
semi: false
proseWrap: always
overrides:
- files: "*.scss"
options:
printWidth: 200

View file

@ -1,4 +1,4 @@
runtime = electron
disturl = https://atom.io/download/electron
target = 1.8.3
target = 1.8.7
arch = x64

View file

@ -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"
}

View file

@ -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',

View file

@ -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 {

View file

@ -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()
}
}

View file

@ -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
}

View file

@ -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, () => {})
}
})
})
}

View file

@ -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 {

View file

@ -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 = {

View file

@ -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) {

View file

@ -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

View 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
}

View file

@ -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)
}

View file

@ -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
View 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
}
}

View file

@ -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()

View file

@ -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__

View file

@ -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', () => {

View 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))
}

View file

@ -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>

View 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>
)
}
}

View file

@ -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: {

View file

@ -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}

View file

@ -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,
},
]

View file

@ -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
}
/>
)
}

View file

@ -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}>

View file

@ -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

View file

@ -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;
}

View file

@ -116,6 +116,6 @@
width: 100%;
border: none;
height: 1px;
border-bottom: var(--base-border);
border-bottom: 1px solid var(--app-menu-divider-color);
}
}

View file

@ -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;

View file

@ -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

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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;
}

View 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)
})
})

View file

@ -153,6 +153,7 @@ const highlighterConfig = merge({}, commonConfig, {
chunkFilename: 'highlighter/[name].js',
},
optimization: {
namedChunks: true,
splitChunks: {
cacheGroups: {
modes: {

View file

@ -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"

View file

@ -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"

View file

@ -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',
}
```