Merge branch 'development' into kuychaco-patch-1

This commit is contained in:
Katrina Uychaco 2019-10-04 18:22:51 -07:00 committed by GitHub
commit dc58c1620b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
104 changed files with 19526 additions and 5672 deletions

View file

@ -3,32 +3,5 @@ coverage:
round: nearest
# thresholds for red to green color coding
range: '10...80'
status:
project:
lib-git:
target: auto
threshold: 5%
base: pr
paths: 'app/src/lib/git'
lib-app:
target: auto
threshold: 5%
base: pr
paths:
- 'app/src/lib/stores'
- 'app/src/lib/databases'
models:
target: auto
threshold: 5%
base: pr
paths: 'app/src/models'
ui-components:
target: auto
threshold: 5%
base: pr
paths: 'app/src/ui'
patch: no
changes: no
status: off
comment: off

View file

@ -67,7 +67,7 @@ to see if the problem has already been reported. If it does exist, add a
:thumbsup: to the issue to indicate this is also an issue for you, and add a
comment to the existing issue if there is extra information you can contribute.
#### How Do I Submit A (Good) Bug Report?
#### How Do I Submit A Bug Report?
Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/).
@ -102,7 +102,7 @@ to see if the enhancement has already been suggested. If it has, add a
:thumbsup: to indicate your interest in it, or comment if there is additional
information you would like to add.
#### How Do I Submit A (Good) Enhancement Suggestion?
#### How Do I Submit An Enhancement Suggestion?
Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/).
@ -150,4 +150,4 @@ These documents are useful resources for contributors to learn more about the p
- [Release Planning](https://github.com/desktop/desktop/blob/development/docs/process/release-planning.md)
- [Issue Triage](https://github.com/desktop/desktop/blob/development/docs/process/issue-triage.md)
- [Issue and Pull Request Labels](https://github.com/desktop/desktop/blob/development/docs/process/labels.md)
- [Pull Request Triage](https://github.com/desktop/desktop/blob/development/docs/process/pull-request-triage.md)
- [Pull Requests](https://github.com/desktop/desktop/blob/development/docs/process/pull-requests.md)

View file

@ -4,68 +4,37 @@ about: Report a problem encountered while using GitHub Desktop
---
<!--
First and foremost, wed like to thank you for taking the time to contribute to our project. Before submitting your issue, please follow these steps:
### Describe the bug
1. Familiarize yourself with our contributing guide:
* https://github.com/desktop/desktop/blob/development/.github/CONTRIBUTING.md#contributing-to-github-desktop
2. Check if your issue (and sometimes workaround) is in the known-issues doc:
* https://github.com/desktop/desktop/blob/development/docs/known-issues.md
3. Make sure your issue isnt a duplicate of another issue
4. If you have made it to this step, go ahead and fill out the template below
-->
A clear and concise description of what the bug is.
## Description
<!--
Provide a detailed description of the behavior you're seeing or the behavior you'd like to see **below** this comment.
-->
### Version & OS
Open 'About GitHub Desktop' menu to see the Desktop version. Also include what operating system you are using.
## Version
<!--
Place the version of GitHub Desktop you have installed **below** this comment. This is displayed under the 'About GitHub Desktop' menu item. If you are running from source, include the commit by running `git rev-parse HEAD` from the local repository.
-->
* GitHub Desktop:
<!--
Place the version of your operating system **below** this comment. The operating system you are running on may also help with reproducing the issue. If you are on macOS, launch 'About This Mac' and write down the OS version listed. If you are on Windows, open 'Command Prompt' and attach the output of this command: 'cmd /c ver'
-->
* Operating system:
### Steps to reproduce the behavior
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
## Steps to Reproduce
<!--
List the steps to reproduce your issue **below** this comment
ex,
1. `step 1`
2. `step 2`
3. `and so on…`
-->
### Expected behavior
### Expected Behavior
<!-- What you expected to happen -->
A clear and concise description of what you expected to happen.
### Actual Behavior
<!-- What actually happens -->
### Actual behavior
A clear and concise description of what actually happened.
## Additional Information
<!--
Place any additional information, configuration, or data that might be necessary to reproduce the issue **below** this comment.
### Screenshots
If you have screen shots or gifs that demonstrate the issue, please include them.
If the issue involves a specific public repository, including the information about it will make it easier to recreate the issue.
If you are dealing with a performance issue or regression, attaching a Performance profile of the task will help the developers understand the runtime behavior of the application on your machine.
https://github.com/desktop/desktop/blob/development/docs/contributing/timeline-profile.md
-->
Add screenshots to help explain your problem, if applicable.
### Logs
<!--
Attach your log file (You can simply drag your file here to insert it) to this issue. Please make sure the generated link to your log file is **below** this comment section otherwise it will not appear when you submit your issue.
macOS logs accessible from the Help menu or location: `~/Library/Application Support/GitHub Desktop/logs/*.desktop.production.log`
Windows logs accessible from the Help menu or location: `%APPDATA%\GitHub Desktop\logs\*.desktop.production.log`
Attach your logs by opening the `Help` menu and selecting `Show Logs...`, if applicable.
The log files are organized by date, so see if anything was generated for today's date.
-->
### Additional context
Add any other context about the problem here.

View file

@ -1,24 +1,17 @@
---
name: "\U0001F389 Problem to raise"
name: "\U00002B50 Submit a request or solve a problem"
about: Surface a problem that you think should be solved
---
<!--
First and foremost, wed like to thank you for taking the time to contribute to our project. Before submitting your issue, please follow these steps:
### Describe the feature or problem youd like to solve
1. Familiarize yourself with our contributing guide:
* https://github.com/desktop/desktop/blob/development/.github/CONTRIBUTING.md#contributing-to-github-desktop
2. Make sure your issue isnt a duplicate of another issue
3. If you have made it to this step, go ahead and fill out the template below
-->
A clear and concise description of what the feature or problem is.
**Please describe the problem you think should be solved**
A clear and concise description of what the problem is and who else might be impacted. Screenshots are encouraged.
### Proposed solution
Example:
How will it benefit Desktop and its users?
> “When I run into a merge conflict, I dont know where to go to resolve it. Anyone who works off of multiple branches will likely run into this problem at least occasionally.”
### Additional context
**[Optional] Do you have any potential solutions in mind?**
A clear and concise description of one or more solutions you think might solve the problem. Please include any considered drawbacks or tradeoffs, and how users might use your solution(s). Screenshots or mockups are helpful here!
Add any other context like screenshots or mockups are helpful, if applicable.

View file

@ -1,40 +1,24 @@
## Overview
<!--
What issue are you addressing? (for example, #1234)
If an issue doesn't exist for this pull request (PR) to address, please open one
to allow for discussion before opening this PR.
You can open a new issue at https://github.com/desktop/desktop/issues/new/choose
What GitHub Desktop issue does this PR address? (for example, #1234)
-->
**Closes #{issue number}**
Closes #[issue number]
## Description
-
### Screenshots
<!--
If this PR touches the UI layer of the app, please include screenshots or animated gifs to show the changes.
-->
## Release notes
<!--
If this is related to a feature, bugfix or improvement, we'd love your help to
summarize these changes to assist with drafting the release notes when this pull
request is merged.
You can leave this blank if you're not sure.
If you don't believe this PR needs to be mentioned in the release notes, write "Notes: no-notes".
Some examples of changelog entries from earlier releases:
- Adds support for Python 3 in GitHub Desktop CLI for macOS users
- Fixes problem with commit being reset when switching between History and Changes tabs
- Fixes caret in co-author selector, which is hidden when dark theme is enabled
- Improves status parsing performance when handling thousands of changed files
-->
Notes:

1
.gitignore vendored
View file

@ -13,3 +13,4 @@ app/node_modules/
*.iml
.envrc
junit*.xml
*.swp

View file

@ -1,10 +1,11 @@
{
"typescript.tsdk": "./node_modules/typescript/lib",
"search.exclude": {
"**/node_modules": true,
".awcache": true,
"**/dist": true,
"**/node_modules": true,
"**/out": true,
".awcache": true
"app/test/fixtures": true
},
"files.exclude": {
"**/.git": true,

View file

@ -1,4 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
yarn-path "./vendor/yarn-1.15.2.js"
yarn-path "./vendor/yarn-1.17.3.js"

View file

@ -1,4 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
yarn-path "../vendor/yarn-1.15.2.js"
yarn-path "../vendor/yarn-1.17.3.js"

View file

@ -3,7 +3,7 @@
"productName": "GitHub Desktop",
"bundleID": "com.github.GitHubClient",
"companyName": "GitHub, Inc.",
"version": "2.1.2",
"version": "2.2.1-beta0",
"main": "./main.js",
"repository": {
"type": "git",

View file

@ -71,8 +71,14 @@ if (!ClientID || !ClientID.length || !ClientSecret || !ClientSecret.length) {
type GitHubAccountType = 'User' | 'Organization'
/** The OAuth scopes we need. */
const Scopes = ['repo', 'user']
/** The OAuth scopes we want to request from GitHub.com. */
const DotComOAuthScopes = ['repo', 'user', 'workflow']
/**
* The OAuth scopes we want to request from GitHub
* Enterprise Server.
*/
const EnterpriseOAuthScopes = ['repo', 'user']
enum HttpStatusCode {
NotModified = 304,
@ -865,6 +871,11 @@ export enum AuthorizationResponseKind {
PersonalAccessTokenBlocked,
Error,
EnterpriseTooOld,
/**
* The API has indicated that the user is required to go through
* the web authentication flow.
*/
WebFlowRequired,
}
export type AuthorizationResponse =
@ -878,6 +889,7 @@ export type AuthorizationResponse =
| { kind: AuthorizationResponseKind.UserRequiresVerification }
| { kind: AuthorizationResponseKind.PersonalAccessTokenBlocked }
| { kind: AuthorizationResponseKind.EnterpriseTooOld }
| { kind: AuthorizationResponseKind.WebFlowRequired }
/**
* Create an authorization with the given login, password, and one-time
@ -901,7 +913,7 @@ export async function createAuthorization(
'POST',
'authorizations',
{
scopes: Scopes,
scopes: getOAuthScopesForEndpoint(endpoint),
client_id: ClientID,
client_secret: ClientSecret,
note: note,
@ -958,6 +970,8 @@ export async function createAuthorization(
) {
// Authorization API does not support providing personal access tokens
return { kind: AuthorizationResponseKind.PersonalAccessTokenBlocked }
} else if (response.status === 410) {
return { kind: AuthorizationResponseKind.WebFlowRequired }
} else if (response.status === 422) {
if (apiError.errors) {
for (const error of apiError.errors) {
@ -1129,7 +1143,8 @@ export function getOAuthAuthorizationURL(
state: string
): string {
const urlBase = getHTMLURL(endpoint)
const scope = encodeURIComponent(Scopes.join(' '))
const scopes = getOAuthScopesForEndpoint(endpoint)
const scope = encodeURIComponent(scopes.join(' '))
return `${urlBase}/login/oauth/authorize?client_id=${ClientID}&scope=${scope}&state=${state}`
}
@ -1157,3 +1172,9 @@ export async function requestOAuthToken(
return null
}
}
function getOAuthScopesForEndpoint(endpoint: string) {
return endpoint === getDotComAPIEndpoint()
? DotComOAuthScopes
: EnterpriseOAuthScopes
}

View file

@ -38,6 +38,7 @@ import { Banner } from '../models/banner'
import { GitRebaseProgress } from '../models/rebase'
import { RebaseFlowStep } from '../models/rebase-flow-step'
import { IStashEntry } from '../models/stash-entry'
import { TutorialStep } from '../models/tutorial-step'
export enum SelectionType {
Repository,
@ -155,9 +156,6 @@ export interface IAppState {
/** The width of the files list in the stash view */
readonly stashedFilesWidth: number
/** Whether we should hide the toolbar (and show inverted window controls) */
readonly titleBarStyle: 'light' | 'dark'
/**
* Used to highlight access keys throughout the app when the
* Alt key is pressed. Only applicable on non-macOS platforms.
@ -230,6 +228,9 @@ export interface IAppState {
* See the ApiRepositoriesStore for more details on loading repositories
*/
readonly apiRepositories: ReadonlyMap<Account, IAccountRepositories>
/** Which step the user is on in the Onboarding Tutorial */
readonly currentOnboardingTutorialStep: TutorialStep
}
export enum FoldoutType {

View file

@ -42,6 +42,14 @@ export interface IDatabaseRepository {
/** The last time the stash entries were checked for the repository */
readonly lastStashCheckDate: number | null
/**
* True if the repository is a tutorial repository created as part
* of the onboarding flow. Tutorial repositories trigger a tutorial
* user experience which introduces new users to some core concepts
* of Git and GitHub.
*/
readonly isTutorialRepository?: boolean
}
/**

View file

@ -6,8 +6,9 @@ import { assertNever } from '../fatal-error'
export enum ExternalEditor {
Atom = 'Atom',
MacVim = 'MacVim',
VisualStudioCode = 'Visual Studio Code',
VisualStudioCodeInsiders = 'Visual Studio Code (Insiders)',
VSCode = 'Visual Studio Code',
VSCodeInsiders = 'Visual Studio Code (Insiders)',
VSCodium = 'VSCodium',
SublimeText = 'Sublime Text',
BBEdit = 'BBEdit',
PhpStorm = 'PhpStorm',
@ -16,6 +17,7 @@ export enum ExternalEditor {
Brackets = 'Brackets',
WebStorm = 'WebStorm',
Typora = 'Typora',
CodeRunner = 'CodeRunner',
SlickEdit = 'SlickEdit',
}
@ -26,12 +28,17 @@ export function parse(label: string): ExternalEditor | null {
if (label === ExternalEditor.MacVim) {
return ExternalEditor.MacVim
}
if (label === ExternalEditor.VisualStudioCode) {
return ExternalEditor.VisualStudioCode
if (label === ExternalEditor.VSCode) {
return ExternalEditor.VSCode
}
if (label === ExternalEditor.VisualStudioCodeInsiders) {
return ExternalEditor.VisualStudioCodeInsiders
if (label === ExternalEditor.VSCodeInsiders) {
return ExternalEditor.VSCodeInsiders
}
if (label === ExternalEditor.VSCodium) {
return ExternalEditor.VSCodium
}
if (label === ExternalEditor.SublimeText) {
return ExternalEditor.SublimeText
}
@ -56,6 +63,9 @@ export function parse(label: string): ExternalEditor | null {
if (label === ExternalEditor.Typora) {
return ExternalEditor.Typora
}
if (label === ExternalEditor.CodeRunner) {
return ExternalEditor.CodeRunner
}
if (label === ExternalEditor.SlickEdit) {
return ExternalEditor.SlickEdit
}
@ -73,10 +83,12 @@ function getBundleIdentifiers(editor: ExternalEditor): ReadonlyArray<string> {
return ['com.github.atom']
case ExternalEditor.MacVim:
return ['org.vim.MacVim']
case ExternalEditor.VisualStudioCode:
case ExternalEditor.VSCode:
return ['com.microsoft.VSCode']
case ExternalEditor.VisualStudioCodeInsiders:
case ExternalEditor.VSCodeInsiders:
return ['com.microsoft.VSCodeInsiders']
case ExternalEditor.VSCodium:
return ['com.visualstudio.code.oss']
case ExternalEditor.SublimeText:
return ['com.sublimetext.3']
case ExternalEditor.BBEdit:
@ -93,6 +105,8 @@ function getBundleIdentifiers(editor: ExternalEditor): ReadonlyArray<string> {
return ['com.jetbrains.WebStorm']
case ExternalEditor.Typora:
return ['abnerworks.Typora']
case ExternalEditor.CodeRunner:
return ['com.krill.CodeRunner']
case ExternalEditor.SlickEdit:
return [
'com.slickedit.SlickEditPro2018',
@ -112,8 +126,8 @@ function getExecutableShim(
switch (editor) {
case ExternalEditor.Atom:
return Path.join(installPath, 'Contents', 'Resources', 'app', 'atom.sh')
case ExternalEditor.VisualStudioCode:
case ExternalEditor.VisualStudioCodeInsiders:
case ExternalEditor.VSCode:
case ExternalEditor.VSCodeInsiders:
return Path.join(
installPath,
'Contents',
@ -122,6 +136,15 @@ function getExecutableShim(
'bin',
'code'
)
case ExternalEditor.VSCodium:
return Path.join(
installPath,
'Contents',
'Resources',
'app',
'bin',
'codium'
)
case ExternalEditor.MacVim:
return Path.join(installPath, 'Contents', 'MacOS', 'MacVim')
case ExternalEditor.SublimeText:
@ -140,6 +163,8 @@ function getExecutableShim(
return Path.join(installPath, 'Contents', 'MacOS', 'WebStorm')
case ExternalEditor.Typora:
return Path.join(installPath, 'Contents', 'MacOS', 'Typora')
case ExternalEditor.CodeRunner:
return Path.join(installPath, 'Contents', 'MacOS', 'CodeRunner')
case ExternalEditor.SlickEdit:
return Path.join(installPath, 'Contents', 'MacOS', 'vs')
default:
@ -181,6 +206,7 @@ export async function getAvailableEditors(): Promise<
macVimPath,
codePath,
codeInsidersPath,
codiumPath,
sublimePath,
bbeditPath,
phpStormPath,
@ -189,12 +215,14 @@ export async function getAvailableEditors(): Promise<
bracketsPath,
webStormPath,
typoraPath,
codeRunnerPath,
slickeditPath,
] = await Promise.all([
findApplication(ExternalEditor.Atom),
findApplication(ExternalEditor.MacVim),
findApplication(ExternalEditor.VisualStudioCode),
findApplication(ExternalEditor.VisualStudioCodeInsiders),
findApplication(ExternalEditor.VSCode),
findApplication(ExternalEditor.VSCodeInsiders),
findApplication(ExternalEditor.VSCodium),
findApplication(ExternalEditor.SublimeText),
findApplication(ExternalEditor.BBEdit),
findApplication(ExternalEditor.PhpStorm),
@ -203,6 +231,7 @@ export async function getAvailableEditors(): Promise<
findApplication(ExternalEditor.Brackets),
findApplication(ExternalEditor.WebStorm),
findApplication(ExternalEditor.Typora),
findApplication(ExternalEditor.CodeRunner),
findApplication(ExternalEditor.SlickEdit),
])
@ -215,16 +244,20 @@ export async function getAvailableEditors(): Promise<
}
if (codePath) {
results.push({ editor: ExternalEditor.VisualStudioCode, path: codePath })
results.push({ editor: ExternalEditor.VSCode, path: codePath })
}
if (codeInsidersPath) {
results.push({
editor: ExternalEditor.VisualStudioCodeInsiders,
editor: ExternalEditor.VSCodeInsiders,
path: codeInsidersPath,
})
}
if (codiumPath) {
results.push({ editor: ExternalEditor.VSCodium, path: codiumPath })
}
if (sublimePath) {
results.push({ editor: ExternalEditor.SublimeText, path: sublimePath })
}
@ -257,6 +290,10 @@ export async function getAvailableEditors(): Promise<
results.push({ editor: ExternalEditor.Typora, path: typoraPath })
}
if (codeRunnerPath) {
results.push({ editor: ExternalEditor.CodeRunner, path: codeRunnerPath })
}
if (slickeditPath) {
results.push({ editor: ExternalEditor.SlickEdit, path: slickeditPath })
}

View file

@ -5,8 +5,9 @@ import { assertNever } from '../fatal-error'
export enum ExternalEditor {
Atom = 'Atom',
VisualStudioCode = 'Visual Studio Code',
VisualStudioCodeInsiders = 'Visual Studio Code (Insiders)',
VSCode = 'Visual Studio Code',
VSCodeInsiders = 'Visual Studio Code (Insiders)',
VSCodium = 'VSCodium',
SublimeText = 'Sublime Text',
Typora = 'Typora',
SlickEdit = 'SlickEdit',
@ -17,12 +18,16 @@ export function parse(label: string): ExternalEditor | null {
return ExternalEditor.Atom
}
if (label === ExternalEditor.VisualStudioCode) {
return ExternalEditor.VisualStudioCode
if (label === ExternalEditor.VSCode) {
return ExternalEditor.VSCode
}
if (label === ExternalEditor.VisualStudioCodeInsiders) {
return ExternalEditor.VisualStudioCode
if (label === ExternalEditor.VSCodeInsiders) {
return ExternalEditor.VSCode
}
if (label === ExternalEditor.VSCodium) {
return ExternalEditor.VSCodium
}
if (label === ExternalEditor.SublimeText) {
@ -48,10 +53,12 @@ async function getEditorPath(editor: ExternalEditor): Promise<string | null> {
switch (editor) {
case ExternalEditor.Atom:
return getPathIfAvailable('/usr/bin/atom')
case ExternalEditor.VisualStudioCode:
case ExternalEditor.VSCode:
return getPathIfAvailable('/usr/bin/code')
case ExternalEditor.VisualStudioCodeInsiders:
case ExternalEditor.VSCodeInsiders:
return getPathIfAvailable('/usr/bin/code-insiders')
case ExternalEditor.VSCodium:
return getPathIfAvailable('/usr/bin/codium')
case ExternalEditor.SublimeText:
return getPathIfAvailable('/usr/bin/subl')
case ExternalEditor.Typora:
@ -84,13 +91,15 @@ export async function getAvailableEditors(): Promise<
atomPath,
codePath,
codeInsidersPath,
codiumPath,
sublimePath,
typoraPath,
slickeditPath,
] = await Promise.all([
getEditorPath(ExternalEditor.Atom),
getEditorPath(ExternalEditor.VisualStudioCode),
getEditorPath(ExternalEditor.VisualStudioCodeInsiders),
getEditorPath(ExternalEditor.VSCode),
getEditorPath(ExternalEditor.VSCodeInsiders),
getEditorPath(ExternalEditor.VSCodium),
getEditorPath(ExternalEditor.SublimeText),
getEditorPath(ExternalEditor.Typora),
getEditorPath(ExternalEditor.SlickEdit),
@ -101,14 +110,15 @@ export async function getAvailableEditors(): Promise<
}
if (codePath) {
results.push({ editor: ExternalEditor.VisualStudioCode, path: codePath })
results.push({ editor: ExternalEditor.VSCode, path: codePath })
}
if (codeInsidersPath) {
results.push({
editor: ExternalEditor.VisualStudioCode,
path: codeInsidersPath,
})
results.push({ editor: ExternalEditor.VSCode, path: codeInsidersPath })
}
if (codiumPath) {
results.push({ editor: ExternalEditor.VSCodium, path: codiumPath })
}
if (sublimePath) {

View file

@ -16,8 +16,9 @@ export enum ExternalEditor {
Atom = 'Atom',
AtomBeta = 'Atom Beta',
AtomNightly = 'Atom Nightly',
VisualStudioCode = 'Visual Studio Code',
VisualStudioCodeInsiders = 'Visual Studio Code (Insiders)',
VSCode = 'Visual Studio Code',
VSCodeInsiders = 'Visual Studio Code (Insiders)',
VSCodium = 'Visual Studio Codium',
SublimeText = 'Sublime Text',
CFBuilder = 'ColdFusion Builder',
Typora = 'Typora',
@ -35,11 +36,14 @@ export function parse(label: string): ExternalEditor | null {
if (label === ExternalEditor.AtomNightly) {
return ExternalEditor.AtomNightly
}
if (label === ExternalEditor.VisualStudioCode) {
return ExternalEditor.VisualStudioCode
if (label === ExternalEditor.VSCode) {
return ExternalEditor.VSCode
}
if (label === ExternalEditor.VisualStudioCodeInsiders) {
return ExternalEditor.VisualStudioCodeInsiders
if (label === ExternalEditor.VSCodeInsiders) {
return ExternalEditor.VSCodeInsiders
}
if (label === ExternalEditor.VSCodium) {
return ExternalEditor.VSCodium
}
if (label === ExternalEditor.SublimeText) {
return ExternalEditor.SublimeText
@ -93,7 +97,7 @@ function getRegistryKeys(
'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\atom-nightly',
},
]
case ExternalEditor.VisualStudioCode:
case ExternalEditor.VSCode:
return [
// 64-bit version of VSCode (user) - provided by default in 64-bit Windows
{
@ -120,7 +124,7 @@ function getRegistryKeys(
'SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{F8A2A208-72B3-4D61-95FC-8A65D340689B}_is1',
},
]
case ExternalEditor.VisualStudioCodeInsiders:
case ExternalEditor.VSCodeInsiders:
return [
// 64-bit version of VSCode (user) - provided by default in 64-bit Windows
{
@ -147,6 +151,33 @@ function getRegistryKeys(
'SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{C26E74D1-022E-4238-8B9D-1E7564A36CC9}_is1',
},
]
case ExternalEditor.VSCodium:
return [
// 64-bit version of VSCodium (user)
{
key: HKEY.HKEY_CURRENT_USER,
subKey:
'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{2E1F05D1-C245-4562-81EE-28188DB6FD17}_is1',
},
// 32-bit version of VSCodium (user)
{
key: HKEY.HKEY_CURRENT_USER,
subKey:
'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{C6065F05-9603-4FC4-8101-B9781A25D88E}}_is1',
},
// 64-bit version of VSCodium (system)
{
key: HKEY.HKEY_LOCAL_MACHINE,
subKey:
'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{D77B7E06-80BA-4137-BCF4-654B95CCEBC5}_is1',
},
// 32-bit version of VSCodium (system)
{
key: HKEY.HKEY_LOCAL_MACHINE,
subKey:
'SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{E34003BB-9E10-4501-8C11-BE3FAA83F23F}_is1',
},
]
case ExternalEditor.SublimeText:
return [
{
@ -286,10 +317,12 @@ function getExecutableShim(
return Path.join(installLocation, 'bin', 'atom-beta.cmd') // remember, CMD must 'useShell'
case ExternalEditor.AtomNightly:
return Path.join(installLocation, 'bin', 'atom-nightly.cmd') // remember, CMD must 'useShell'
case ExternalEditor.VisualStudioCode:
case ExternalEditor.VSCode:
return Path.join(installLocation, 'bin', 'code.cmd') // remember, CMD must 'useShell'
case ExternalEditor.VisualStudioCodeInsiders:
case ExternalEditor.VSCodeInsiders:
return Path.join(installLocation, 'bin', 'code-insiders.cmd') // remember, CMD must 'useShell'
case ExternalEditor.VSCodium:
return Path.join(installLocation, 'bin', 'codium.cmd') // remember, CMD must 'useShell'
case ExternalEditor.SublimeText:
return Path.join(installLocation, 'subl.exe')
case ExternalEditor.CFBuilder:
@ -324,16 +357,18 @@ function isExpectedInstallation(
return displayName === 'Atom Beta' && publisher === 'GitHub Inc.'
case ExternalEditor.AtomNightly:
return displayName === 'Atom Nightly' && publisher === 'GitHub Inc.'
case ExternalEditor.VisualStudioCode:
case ExternalEditor.VSCode:
return (
displayName.startsWith('Microsoft Visual Studio Code') &&
publisher === 'Microsoft Corporation'
)
case ExternalEditor.VisualStudioCodeInsiders:
case ExternalEditor.VSCodeInsiders:
return (
displayName.startsWith('Microsoft Visual Studio Code Insiders') &&
publisher === 'Microsoft Corporation'
)
case ExternalEditor.VSCodium:
return displayName === 'Visual Source Codium' && publisher === 'VSCodium'
case ExternalEditor.SublimeText:
return (
displayName === 'Sublime Text' && publisher === 'Sublime HQ Pty Ltd'
@ -377,21 +412,11 @@ function extractApplicationInformation(
editor: ExternalEditor,
keys: ReadonlyArray<RegistryValue>
): { displayName: string; publisher: string; installLocation: string } {
if (editor === ExternalEditor.Atom) {
const displayName = getKeyOrEmpty(keys, 'DisplayName')
const publisher = getKeyOrEmpty(keys, 'Publisher')
const installLocation = getKeyOrEmpty(keys, 'InstallLocation')
return { displayName, publisher, installLocation }
}
if (editor === ExternalEditor.AtomBeta) {
const displayName = getKeyOrEmpty(keys, 'DisplayName')
const publisher = getKeyOrEmpty(keys, 'Publisher')
const installLocation = getKeyOrEmpty(keys, 'InstallLocation')
return { displayName, publisher, installLocation }
}
if (editor === ExternalEditor.AtomNightly) {
if (
editor === ExternalEditor.Atom ||
editor === ExternalEditor.AtomBeta ||
editor === ExternalEditor.AtomNightly
) {
const displayName = getKeyOrEmpty(keys, 'DisplayName')
const publisher = getKeyOrEmpty(keys, 'Publisher')
const installLocation = getKeyOrEmpty(keys, 'InstallLocation')
@ -399,8 +424,8 @@ function extractApplicationInformation(
}
if (
editor === ExternalEditor.VisualStudioCode ||
editor === ExternalEditor.VisualStudioCodeInsiders
editor === ExternalEditor.VSCode ||
editor === ExternalEditor.VSCodeInsiders
) {
const displayName = getKeyOrEmpty(keys, 'DisplayName')
const publisher = getKeyOrEmpty(keys, 'Publisher')
@ -408,6 +433,13 @@ function extractApplicationInformation(
return { displayName, publisher, installLocation }
}
if (editor === ExternalEditor.VSCodium) {
const displayName = getKeyOrEmpty(keys, 'DisplayName')
const publisher = getKeyOrEmpty(keys, 'Publisher')
const installLocation = getKeyOrEmpty(keys, 'InstallLocation')
return { displayName, publisher, installLocation }
}
if (editor === ExternalEditor.SublimeText) {
let displayName = ''
let publisher = ''
@ -546,6 +578,7 @@ export async function getAvailableEditors(): Promise<
atomNightlyPath,
codePath,
codeInsidersPath,
codiumPath,
sublimePath,
cfBuilderPath,
typoraPath,
@ -554,8 +587,9 @@ export async function getAvailableEditors(): Promise<
findApplication(ExternalEditor.Atom),
findApplication(ExternalEditor.AtomBeta),
findApplication(ExternalEditor.AtomNightly),
findApplication(ExternalEditor.VisualStudioCode),
findApplication(ExternalEditor.VisualStudioCodeInsiders),
findApplication(ExternalEditor.VSCode),
findApplication(ExternalEditor.VSCodeInsiders),
findApplication(ExternalEditor.VSCodium),
findApplication(ExternalEditor.SublimeText),
findApplication(ExternalEditor.CFBuilder),
findApplication(ExternalEditor.Typora),
@ -588,7 +622,7 @@ export async function getAvailableEditors(): Promise<
if (codePath) {
results.push({
editor: ExternalEditor.VisualStudioCode,
editor: ExternalEditor.VSCode,
path: codePath,
usesShell: true,
})
@ -596,12 +630,20 @@ export async function getAvailableEditors(): Promise<
if (codeInsidersPath) {
results.push({
editor: ExternalEditor.VisualStudioCodeInsiders,
editor: ExternalEditor.VSCodeInsiders,
path: codeInsidersPath,
usesShell: true,
})
}
if (codiumPath) {
results.push({
editor: ExternalEditor.VSCodium,
path: codiumPath,
usesShell: true,
})
}
if (sublimePath) {
results.push({
editor: ExternalEditor.SublimeText,

View file

@ -1,4 +1,7 @@
import { IAPIEmail } from './api'
import * as URL from 'url'
import { IAPIEmail, getDotComAPIEndpoint } from './api'
import { Account } from '../models/account'
/**
* Lookup a suitable email address to display in the application, based on the
@ -13,9 +16,9 @@ import { IAPIEmail } from './api'
*
* @param emails array of email addresses associated with an account
*/
export function lookupPreferredEmail(
emails: ReadonlyArray<IAPIEmail>
): IAPIEmail | null {
export function lookupPreferredEmail(account: Account): IAPIEmail | null {
const emails = account.emails
if (emails.length === 0) {
return null
}
@ -25,9 +28,12 @@ export function lookupPreferredEmail(
return primary
}
const stealthSuffix = `@${getStealthEmailHostForEndpoint(account.endpoint)}`
const noReply = emails.find(e =>
e.email.toLowerCase().endsWith('@users.noreply.github.com')
e.email.toLowerCase().endsWith(stealthSuffix)
)
if (noReply) {
return noReply
}
@ -58,3 +64,15 @@ export function getDefaultEmail(emails: ReadonlyArray<IAPIEmail>): string {
return emails[0].email || ''
}
/**
* Returns the stealth email host name for a given endpoint. The stealth
* email host is hardcoded to the subdomain users.noreply under the
* endpoint host.
*/
function getStealthEmailHostForEndpoint(endpoint: string) {
const url = URL.parse(endpoint)
return getDotComAPIEndpoint() !== endpoint
? `users.noreply.${url.hostname}`
: 'users.noreply.github.com'
}

View file

@ -105,3 +105,11 @@ export function enableBranchProtectionWarningFlow(): boolean {
export function enableHideWhitespaceInDiffOption(): boolean {
return enableBetaFeatures()
}
/**
* Should we enable the onboarding tutorial. This includes the initial
* configuration of the tutorial repo as well as the tutorial itself.
*/
export function enableTutorial(): boolean {
return true
}

View file

@ -23,15 +23,12 @@ export async function createBranch(
const args =
startPoint !== null ? ['branch', name, startPoint] : ['branch', name]
try {
await git(args, repository.path, 'createBranch')
const branches = await getBranches(repository, `refs/heads/${name}`)
if (branches.length > 0) {
return branches[0]
}
} catch (err) {
log.error('createBranch failed', err)
await git(args, repository.path, 'createBranch')
const branches = await getBranches(repository, `refs/heads/${name}`)
if (branches.length > 0) {
return branches[0]
}
return null
}

View file

@ -1,15 +1,17 @@
import { GitError as DugiteError } from 'dugite'
import { git, GitError } from './core'
import { Repository } from '../../models/repository'
import {
IStashEntry,
StashedChangesLoadStates,
StashedFileChanges,
} from '../../models/stash-entry'
import { CommittedFileChange } from '../../models/status'
import { git, GitError } from './core'
import {
WorkingDirectoryFileChange,
CommittedFileChange,
} from '../../models/status'
import { parseChangedFiles } from './log'
import { stageFiles } from './update-index'
export const DesktopStashEntryMarker = '!!GitHub_Desktop'
@ -113,10 +115,20 @@ export function createDesktopStashMessage(branchName: string) {
*/
export async function createDesktopStashEntry(
repository: Repository,
branchName: string
branchName: string,
untrackedFilesToStage: ReadonlyArray<WorkingDirectoryFileChange>
): Promise<true> {
// We must ensure that no untracked files are present before stashing
// See https://github.com/desktop/desktop/pull/8085
// First ensure that all changes in file are selected
// (in case the user has not explicitly checked the checkboxes for the untracked files)
const fullySelectedUntrackedFiles = untrackedFilesToStage.map(x =>
x.withIncludeAll(true)
)
await stageFiles(repository, fullySelectedUntrackedFiles)
const message = createDesktopStashMessage(branchName)
const args = ['stash', 'push', '--include-untracked', '-m', message]
const args = ['stash', 'push', '-m', message]
const result = await git(args, repository.path, 'createStashEntry', {
successExitCodes: new Set<number>([0, 1]),

View file

@ -30,6 +30,9 @@ export class APIError extends Error {
/** The error as sent from the API, if one could be parsed. */
public readonly apiError: IAPIError | null
/** The HTTP response code that the error was delivered with */
public readonly responseStatus: number
public constructor(response: Response, apiError: IAPIError | null) {
let message
if (apiError && apiError.message) {
@ -48,6 +51,7 @@ export class APIError extends Error {
super(message)
this.responseStatus = response.status
this.apiError = apiError
}
}

View file

@ -268,6 +268,67 @@ export interface IDailyMeasures {
* the suggested next steps view
*/
readonly suggestedStepViewStash: number
/**
* _[Onboarding tutorial]_
* Has the user clicked the button to start the onboarding tutorial?
*/
readonly tutorialStarted: boolean
/**
* _[Onboarding tutorial]_
* Has the user successfully created a tutorial repo?
*/
readonly tutorialRepoCreated: boolean
/**
* _[Onboarding tutorial]_
* Has the user installed an editor, skipped this step, or have an editor already installed?
*/
readonly tutorialEditorInstalled: boolean
/**
* _[Onboarding tutorial]_
* Has the user successfully completed the create a branch step?
*/
readonly tutorialBranchCreated: boolean
/**
* _[Onboarding tutorial]_
* Has the user completed the edit a file step?
*/
readonly tutorialFileEdited: boolean
/**
* _[Onboarding tutorial]_
* Has the user completed the commit a file change step?
*/
readonly tutorialCommitCreated: boolean
/**
* _[Onboarding tutorial]_
* Has the user completed the push a branch step?
*/
readonly tutorialBranchPushed: boolean
/**
* _[Onboarding tutorial]_
* Has the user compeleted the create a PR step?
*/
readonly tutorialPrCreated: boolean
/**
* _[Onboarding tutorial]_
* Has the user completed all tutorial steps?
*/
readonly tutorialCompleted: boolean
/**
* _[Onboarding tutorial]_
* What's the highest tutorial step completed by user?
* (`0` is tutorial created, first step is `1`)
*/
readonly highestTutorialStepCompleted: number
}
export class StatsDatabase extends Dexie {

View file

@ -112,6 +112,17 @@ const DefaultDailyMeasures: IDailyMeasures = {
suggestedStepViewStash: 0,
commitsToProtectedBranch: 0,
commitsToRepositoryWithBranchProtections: 0,
tutorialStarted: false,
tutorialRepoCreated: false,
tutorialEditorInstalled: false,
tutorialBranchCreated: false,
tutorialFileEdited: false,
tutorialCommitCreated: false,
tutorialBranchPushed: false,
tutorialPrCreated: false,
tutorialCompleted: false,
// this is `-1` because `0` signifies "tutorial created"
highestTutorialStepCompleted: -1,
}
interface IOnboardingStats {
@ -1185,6 +1196,87 @@ export class StatsStore implements IStatsStore {
}))
}
/**
* Onboarding tutorial metrics
*/
public recordTutorialStarted() {
return this.updateDailyMeasures(() => ({
tutorialStarted: true,
}))
}
public recordTutorialRepoCreated() {
return this.updateDailyMeasures(() => ({
tutorialRepoCreated: true,
}))
}
public recordTutorialEditorInstalled() {
return this.updateDailyMeasures(() => ({
tutorialEditorInstalled: true,
}))
}
public recordTutorialBranchCreated() {
return this.updateDailyMeasures(() => ({
tutorialEditorInstalled: true,
tutorialBranchCreated: true,
}))
}
public recordTutorialFileEdited() {
return this.updateDailyMeasures(() => ({
tutorialEditorInstalled: true,
tutorialBranchCreated: true,
tutorialFileEdited: true,
}))
}
public recordTutorialCommitCreated() {
return this.updateDailyMeasures(() => ({
tutorialEditorInstalled: true,
tutorialBranchCreated: true,
tutorialFileEdited: true,
tutorialCommitCreated: true,
}))
}
public recordTutorialBranchPushed() {
return this.updateDailyMeasures(() => ({
tutorialEditorInstalled: true,
tutorialBranchCreated: true,
tutorialFileEdited: true,
tutorialCommitCreated: true,
tutorialBranchPushed: true,
}))
}
public recordTutorialPrCreated() {
return this.updateDailyMeasures(() => ({
tutorialEditorInstalled: true,
tutorialBranchCreated: true,
tutorialFileEdited: true,
tutorialCommitCreated: true,
tutorialBranchPushed: true,
tutorialPrCreated: true,
}))
}
public recordTutorialCompleted() {
return this.updateDailyMeasures(() => ({
tutorialCompleted: true,
}))
}
public recordHighestTutorialStepCompleted(step: number) {
return this.updateDailyMeasures(m => ({
highestTutorialStepCompleted: Math.max(
step,
m.highestTutorialStepCompleted
),
}))
}
/** Post some data to our stats endpoint. */
private post(body: object): Promise<Response> {
const options: RequestInit = {

View file

@ -6,6 +6,7 @@ import {
isConflictWithMarkers,
GitStatusEntry,
isConflictedFileStatus,
WorkingDirectoryFileChange,
} from '../models/status'
import { assertNever } from './fatal-error'
import {
@ -135,6 +136,15 @@ export function getUnmergedFiles(status: WorkingDirectoryStatus) {
return status.files.filter(f => isConflictedFile(f.status))
}
/** Filter working directory changes for untracked files */
export function getUntrackedFiles(
workingDirectoryStatus: WorkingDirectoryStatus
): ReadonlyArray<WorkingDirectoryFileChange> {
return workingDirectoryStatus.files.filter(
file => file.status.kind === AppFileStatusKind.Untracked
)
}
/** Filter working directory changes for resolved files */
export function getResolvedFiles(
status: WorkingDirectoryStatus,

View file

@ -78,19 +78,9 @@ export class AccountsStore extends TypedBaseStore<ReadonlyArray<Account>> {
public async addAccount(account: Account): Promise<Account | null> {
await this.loadingPromise
let updated = account
try {
updated = await updatedAccount(account)
} catch (e) {
log.warn(`Failed to fetch user ${account.login}`, e)
}
try {
await this.secureStore.setItem(
getKeyForAccount(updated),
updated.login,
updated.token
)
const key = getKeyForAccount(account)
await this.secureStore.setItem(key, account.login, account.token)
} catch (e) {
log.error(`Error adding account '${account.login}'`, e)
@ -106,10 +96,16 @@ export class AccountsStore extends TypedBaseStore<ReadonlyArray<Account>> {
return null
}
this.accounts = [...this.accounts, updated]
const accountsByEndpoint = this.accounts.reduce(
(map, x) => map.set(x.endpoint, x),
new Map<string, Account>()
)
accountsByEndpoint.set(account.endpoint, account)
this.accounts = [...accountsByEndpoint.values()]
this.save()
return updated
return account
}
/** Refresh all accounts by fetching their latest info from the API. */
@ -159,7 +155,9 @@ export class AccountsStore extends TypedBaseStore<ReadonlyArray<Account>> {
return
}
this.accounts = this.accounts.filter(a => a.id !== account.id)
this.accounts = this.accounts.filter(
a => !(a.endpoint === account.endpoint && a.id === account.id)
)
this.save()
}

View file

@ -14,11 +14,7 @@ import {
import { Account } from '../../models/account'
import { AppMenu, IMenu } from '../../models/app-menu'
import { IAuthor } from '../../models/author'
import {
Branch,
eligibleForFastForward,
IAheadBehind,
} from '../../models/branch'
import { Branch, IAheadBehind } from '../../models/branch'
import { BranchesTab } from '../../models/branches-tab'
import { CloneRepositoryTab } from '../../models/clone-repository-tab'
import { CloningRepository } from '../../models/cloning-repository'
@ -79,6 +75,7 @@ import {
getDotComAPIEndpoint,
IAPIOrganization,
IAPIBranch,
IAPIRepository,
} from '../api'
import { shell } from '../app-shell'
import {
@ -242,13 +239,14 @@ import { RebaseFlowStep, RebaseStep } from '../../models/rebase-flow-step'
import { arrayEquals } from '../equality'
import { MenuLabelsEvent } from '../../models/menu-labels'
import { findRemoteBranchName } from './helpers/find-branch-name'
/**
* As fast-forwarding local branches is proportional to the number of local
* branches, and is run after every fetch/push/pull, this is skipped when the
* number of eligible branches is greater than a given threshold.
*/
const FastForwardBranchesThreshold = 20
import { findBranchesForFastForward } from './helpers/find-branches-for-fast-forward'
import {
TutorialStep,
orderedTutorialSteps,
isValidTutorialStep,
} from '../../models/tutorial-step'
import { OnboardingTutorialAssessor } from './helpers/tutorial-assessor'
import { getUntrackedFiles } from '../status'
const LastSelectedRepositoryIDKey = 'last-selected-repository-id'
@ -384,6 +382,10 @@ export class AppStore extends TypedBaseStore<IAppState> {
private hasUserViewedStash = false
/** Which step the user needs to complete next in the onboarding tutorial */
private currentOnboardingTutorialStep = TutorialStep.NotApplicable
private readonly tutorialAssessor: OnboardingTutorialAssessor
public constructor(
private readonly gitHubUserStore: GitHubUserStore,
private readonly cloningRepositoriesStore: CloningRepositoriesStore,
@ -415,6 +417,88 @@ export class AppStore extends TypedBaseStore<IAppState> {
this.wireupIpcEventHandlers(window)
this.wireupStoreEventHandlers()
getAppMenu()
this.tutorialAssessor = new OnboardingTutorialAssessor(
this.getResolvedExternalEditor
)
}
/** Figure out what step of the tutorial the user needs to do next */
private async updateCurrentTutorialStep(
repository: Repository
): Promise<void> {
const currentStep = await this.tutorialAssessor.getCurrentStep(
repository.isTutorialRepository,
this.repositoryStateCache.get(repository)
)
log.info(`Current tutorial step is ${currentStep}`)
// only emit an update if its changed
if (currentStep !== this.currentOnboardingTutorialStep) {
this.currentOnboardingTutorialStep = currentStep
this.recordTutorialStepCompleted(currentStep)
this.emitUpdate()
}
}
private recordTutorialStepCompleted(step: TutorialStep): void {
if (!isValidTutorialStep(step)) {
return
}
this.statsStore.recordHighestTutorialStepCompleted(
orderedTutorialSteps.indexOf(step)
)
switch (step) {
case TutorialStep.PickEditor:
// don't need to record anything for the first step
break
case TutorialStep.CreateBranch:
this.statsStore.recordTutorialEditorInstalled()
break
case TutorialStep.EditFile:
this.statsStore.recordTutorialBranchCreated()
break
case TutorialStep.MakeCommit:
this.statsStore.recordTutorialFileEdited()
break
case TutorialStep.PushBranch:
this.statsStore.recordTutorialCommitCreated()
break
case TutorialStep.OpenPullRequest:
this.statsStore.recordTutorialBranchPushed()
break
case TutorialStep.AllDone:
this.statsStore.recordTutorialPrCreated()
this.statsStore.recordTutorialCompleted()
break
default:
assertNever(step, 'Unaccounted for step type')
}
}
public async _resumeTutorial(repository: Repository) {
this.tutorialAssessor.resumeTutorial()
await this.updateCurrentTutorialStep(repository)
}
public async _pauseTutorial(repository: Repository) {
this.tutorialAssessor.pauseTutorial()
await this.updateCurrentTutorialStep(repository)
}
/** Call via `Dispatcher` when the user opts to skip the pick editor step of the onboarding tutorial */
public async _skipPickEditorTutorialStep(repository: Repository) {
this.tutorialAssessor.skipPickEditor()
await this.updateCurrentTutorialStep(repository)
}
/**
* Call via `Dispatcher` when the user has either created a pull request or opts to
* skip the create pull request step of the onboarding tutorial
*/
public async _markPullRequestTutorialStepAsComplete(repository: Repository) {
this.tutorialAssessor.markPullRequestTutorialStepAsComplete()
await this.updateCurrentTutorialStep(repository)
}
private wireupIpcEventHandlers(window: Electron.BrowserWindow) {
@ -607,8 +691,6 @@ export class AppStore extends TypedBaseStore<IAppState> {
commitSummaryWidth: this.commitSummaryWidth,
stashedFilesWidth: this.stashedFilesWidth,
appMenuState: this.appMenu ? this.appMenu.openMenus : [],
titleBarStyle:
this.showWelcomeFlow || repositories.length === 0 ? 'light' : 'dark',
highlightAccessKeys: this.highlightAccessKeys,
isUpdateAvailableBannerVisible: this.isUpdateAvailableBannerVisible,
currentBanner: this.currentBanner,
@ -628,6 +710,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
automaticallySwitchTheme: this.automaticallySwitchTheme,
apiRepositories: this.apiRepositoriesStore.getState(),
optOutOfUsageTracking: this.statsStore.getOptOut(),
currentOnboardingTutorialStep: this.currentOnboardingTutorialStep,
}
}
@ -1294,6 +1377,18 @@ export class AppStore extends TypedBaseStore<IAppState> {
): Promise<Repository | null> {
const previouslySelectedRepository = this.selectedRepository
// do this quick check to see if we have a tutorial respository
// cause if its not we can quickly hide the tutorial pane
// in the first `emitUpdate` below
const previouslyInTutorial =
this.currentOnboardingTutorialStep !== TutorialStep.NotApplicable
if (
previouslyInTutorial &&
(!(repository instanceof Repository) || !repository.isTutorialRepository)
) {
this.currentOnboardingTutorialStep = TutorialStep.NotApplicable
}
this.selectedRepository = repository
this.emitUpdate()
@ -1658,6 +1753,14 @@ export class AppStore extends TypedBaseStore<IAppState> {
this.selectedExternalEditor = externalEditorValue
}
// Deferred, attempts to resolve the user's selected editor (i.e.
// ensures that it's actually present on the machine), needs to
// happen after the tutorial assessor has been initialized, see:
// https://github.com/desktop/desktop/pull/8242#pullrequestreview-289936574
this._resolveCurrentEditor().catch(e =>
log.error('Failed resolving current editor at startup', e)
)
const shellValue = localStorage.getItem(shellKey)
this.selectedShell = shellValue ? parseShell(shellValue) : DefaultShell
@ -2578,6 +2681,8 @@ export class AppStore extends TypedBaseStore<IAppState> {
this.updateMenuItemLabels(latestState)
this._initializeCompare(repository)
this.updateCurrentTutorialStep(repository)
}
private async updateStashEntryCountMetric(
@ -3038,7 +3143,11 @@ export class AppStore extends TypedBaseStore<IAppState> {
if (hasDeletedFiles && !transientStashEntry) {
const gitStore = this.gitStoreCache.get(repository)
const stashCreated = await gitStore.performFailableOperation(() => {
return createDesktopStashEntry(repository, branch.name)
return createDesktopStashEntry(
repository,
branch.name,
getUntrackedFiles(changesState.workingDirectory)
)
})
if (stashCreated) {
@ -3084,7 +3193,8 @@ export class AppStore extends TypedBaseStore<IAppState> {
repository.path,
repository.id,
skeletonGitHubRepository,
repository.missing
repository.missing,
false
)
const account = getAccountForEndpoint(
@ -3586,31 +3696,9 @@ export class AppStore extends TypedBaseStore<IAppState> {
}
private async fastForwardBranches(repository: Repository) {
const state = this.repositoryStateCache.get(repository)
const branches = state.branchesState.allBranches
const { branchesState } = this.repositoryStateCache.get(repository)
const tip = state.branchesState.tip
const currentBranchName =
tip.kind === TipState.Valid ? tip.branch.name : null
let eligibleBranches = branches.filter(b =>
eligibleForFastForward(b, currentBranchName)
)
if (eligibleBranches.length >= FastForwardBranchesThreshold) {
log.info(
`skipping fast-forward for all branches as there are ${
eligibleBranches.length
} local branches - this will run again when there are less than ${FastForwardBranchesThreshold} local branches tracking remotes`
)
const defaultBranch = state.branchesState.defaultBranch
eligibleBranches =
defaultBranch != null &&
eligibleForFastForward(defaultBranch, currentBranchName)
? [defaultBranch, ...state.branchesState.recentBranches]
: []
}
const eligibleBranches = findBranchesForFastForward(branchesState)
for (const branch of eligibleBranches) {
const aheadBehind = await getBranchAheadBehind(repository, branch)
@ -4306,14 +4394,16 @@ export class AppStore extends TypedBaseStore<IAppState> {
return Promise.resolve()
}
public _setExternalEditor(selectedEditor: ExternalEditor): Promise<void> {
public async _setExternalEditor(selectedEditor: ExternalEditor) {
this.selectedExternalEditor = selectedEditor
localStorage.setItem(externalEditorKey, selectedEditor)
this.emitUpdate()
this.updateMenuLabelsForSelectedRepository()
return Promise.resolve()
// Make sure we keep the resolved (cached) editor
// in sync when the user changes their editor choice.
await this._resolveCurrentEditor()
}
public _setShell(shell: Shell): Promise<void> {
@ -4460,6 +4550,10 @@ export class AppStore extends TypedBaseStore<IAppState> {
if (this.appIsFocused) {
if (this.selectedRepository instanceof Repository) {
this.startPullRequestUpdater(this.selectedRepository)
// if we're in the tutorial and we don't have an editor yet, check for one!
if (this.currentOnboardingTutorialStep === TutorialStep.PickEditor) {
await this._resolveCurrentEditor()
}
}
} else {
this.stopPullRequestUpdater()
@ -4513,7 +4607,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
return this.accountsStore.removeAccount(account)
}
public async _addAccount(account: Account): Promise<void> {
private async _addAccount(account: Account): Promise<void> {
log.info(
`[AppStore] adding account ${account.login} (${account.name}) to store`
)
@ -4561,6 +4655,40 @@ export class AppStore extends TypedBaseStore<IAppState> {
return this.repositoriesStore.updateRepositoryMissing(repository, missing)
}
/**
* Add a tutorial repository.
*
* This method differs from the `_addRepositories` method in that it
* requires that the repository has been created on the remote and
* set up to track it. Given that tutorial repositories are created
* from the no-repositories blank slate it shouldn't be possible for
* another repository with the same path to exist in the repositories
* table but in case that hanges in the future this method will set
* the tutorial flag on the existing repository at the given path.
*/
public async _addTutorialRepository(
path: string,
endpoint: string,
apiRepository: IAPIRepository
) {
const validatedPath = await validatedRepositoryPath(path)
if (validatedPath) {
log.info(
`[AppStore] adding tutorial repository at ${validatedPath} to store`
)
await this.repositoriesStore.addTutorialRepository(
validatedPath,
endpoint,
apiRepository
)
this.tutorialAssessor.onNewTutorialRepository()
} else {
const error = new Error(`${path} isn't a git repository.`)
this.emitError(error)
}
}
public async _addRepositories(
paths: ReadonlyArray<string>
): Promise<ReadonlyArray<Repository>> {
@ -4812,6 +4940,18 @@ export class AppStore extends TypedBaseStore<IAppState> {
return Promise.resolve()
}
public async _showGitHubExplore(repository: Repository): Promise<void> {
const { gitHubRepository } = repository
if (!gitHubRepository || gitHubRepository.htmlURL === null) {
return
}
const url = new URL(gitHubRepository.htmlURL)
url.pathname = '/explore'
await this._openInBrowser(url.toString())
}
public async _createPullRequest(repository: Repository): Promise<void> {
const gitHubRepository = repository.gitHubRepository
if (!gitHubRepository) {
@ -4955,6 +5095,10 @@ export class AppStore extends TypedBaseStore<IAppState> {
}/pull/new/${urlEncodedBranchName}`
await this._openInBrowser(baseURL)
if (this.currentOnboardingTutorialStep === TutorialStep.OpenPullRequest) {
this._markPullRequestTutorialStepAsComplete(repository)
}
}
public async _updateExistingUpstreamRemote(
@ -5128,10 +5272,23 @@ export class AppStore extends TypedBaseStore<IAppState> {
const resolvedExternalEditor = match != null ? match.editor : null
if (this.resolvedExternalEditor !== resolvedExternalEditor) {
this.resolvedExternalEditor = resolvedExternalEditor
// Make sure we let the tutorial assesor know that we have a new editor
// in case it's stuck waiting for one to be selected.
if (this.currentOnboardingTutorialStep === TutorialStep.PickEditor) {
if (this.selectedRepository instanceof Repository) {
this.updateCurrentTutorialStep(this.selectedRepository)
}
}
this.emitUpdate()
}
}
public getResolvedExternalEditor = () => {
return this.resolvedExternalEditor
}
/** This shouldn't be called directly. See `Dispatcher`. */
public _updateManualConflictResolution(
repository: Repository,
@ -5207,7 +5364,15 @@ export class AppStore extends TypedBaseStore<IAppState> {
)
}
await createDesktopStashEntry(repository, branchName)
const {
changesState: { workingDirectory },
} = this.repositoryStateCache.get(repository)
await createDesktopStashEntry(
repository,
branchName,
getUntrackedFiles(workingDirectory)
)
}
/** This shouldn't be called directly. See `Dispatcher`. */
@ -5219,9 +5384,16 @@ export class AppStore extends TypedBaseStore<IAppState> {
return
}
const {
changesState: { workingDirectory },
} = this.repositoryStateCache.get(repository)
const gitStore = this.gitStoreCache.get(repository)
const isStashCreated = await gitStore.performFailableOperation(() => {
return createDesktopStashEntry(repository, branchToCheckout)
return createDesktopStashEntry(
repository,
branchToCheckout,
getUntrackedFiles(workingDirectory)
)
})
if (!isStashCreated) {

View file

@ -0,0 +1,48 @@
import { IBranchesState } from '../../app-state'
import { eligibleForFastForward, Branch } from '../../../models/branch'
import { TipState } from '../../../models/tip'
/**
* As fast-forwarding local branches is proportional to the number of local
* branches, and is run after every fetch/push/pull, this is skipped when the
* number of eligible branches is greater than a given threshold.
*/
const FastForwardBranchesThreshold = 20
/** Figured out what branches are eligible to fast forward
*
* If all eligible branches count is more than `FastForwardBranchesThreshold`,
* returns a shorter list of default and recent branches
*
* @param branchesState current branchesState for a repository
* @returns list of branches eligible for fast forward
*/
export function findBranchesForFastForward(
branchesState: IBranchesState
): ReadonlyArray<Branch> {
const { allBranches, tip, defaultBranch, recentBranches } = branchesState
const currentBranchName = tip.kind === TipState.Valid ? tip.branch.name : null
const allEligibleBranches = allBranches.filter(b =>
eligibleForFastForward(b, currentBranchName)
)
if (allEligibleBranches.length < FastForwardBranchesThreshold) {
return allEligibleBranches
}
log.info(
`skipping fast-forward for all branches as there are ${
allEligibleBranches.length
} eligible branches (Threshold is ${FastForwardBranchesThreshold} eligible branches).`
)
// we don't have to worry about this being a duplicate, because recent branches
// never include the default branch (at least right now)
const shortListBranches =
defaultBranch !== null ? [...recentBranches, defaultBranch] : recentBranches
const eligibleShortListBranches = shortListBranches.filter(b =>
eligibleForFastForward(b, currentBranchName)
)
return eligibleShortListBranches
}

View file

@ -0,0 +1,172 @@
import { IRepositoryState } from '../../app-state'
import { TutorialStep } from '../../../models/tutorial-step'
import { TipState } from '../../../models/tip'
import { ExternalEditor } from '../../editors'
import { setBoolean, getBoolean } from '../../local-storage'
const skipInstallEditorKey = 'tutorial-install-editor-skipped'
const pullRequestStepCompleteKey = 'tutorial-pull-request-step-complete'
const tutorialPausedKey = 'tutorial-paused'
/**
* Used to determine which step of the onboarding
* tutorial the user needs to complete next
*
* Stores some state that only it needs to know about. The
* actual step result is stored in App Store so the rest of
* the app can access it.
*/
export class OnboardingTutorialAssessor {
/** Has the user opted to skip the install editor step? */
private installEditorSkipped: boolean = getBoolean(
skipInstallEditorKey,
false
)
/** Has the user opted to skip the create pull request step? */
private prStepComplete: boolean = getBoolean(
pullRequestStepCompleteKey,
false
)
/** Is the tutorial currently paused? */
private tutorialPaused: boolean = getBoolean(tutorialPausedKey, false)
public constructor(
/** Method to call when we need to get the current editor */
private getResolvedExternalEditor: () => ExternalEditor | null
) {}
/** Determines what step the user needs to complete next in the Onboarding Tutorial */
public async getCurrentStep(
isTutorialRepo: boolean,
repositoryState: IRepositoryState
): Promise<TutorialStep> {
if (!isTutorialRepo) {
// If a new repo has been added, we can unpause the tutorial repo
// as we will no longer present the no-repos blank slate view resume button
// Fixes https://github.com/desktop/desktop/issues/8341
if (this.tutorialPaused) {
this.resumeTutorial()
}
return TutorialStep.NotApplicable
} else if (this.tutorialPaused) {
return TutorialStep.Paused
} else if (!(await this.isEditorInstalled())) {
return TutorialStep.PickEditor
} else if (!this.isBranchCheckedOut(repositoryState)) {
return TutorialStep.CreateBranch
} else if (!this.hasChangedFile(repositoryState)) {
return TutorialStep.EditFile
} else if (!this.hasMultipleCommits(repositoryState)) {
return TutorialStep.MakeCommit
} else if (!this.commitPushed(repositoryState)) {
return TutorialStep.PushBranch
} else if (!this.pullRequestCreated(repositoryState)) {
return TutorialStep.OpenPullRequest
} else {
return TutorialStep.AllDone
}
}
private async isEditorInstalled(): Promise<boolean> {
return (
this.installEditorSkipped || this.getResolvedExternalEditor() !== null
)
}
private isBranchCheckedOut(repositoryState: IRepositoryState): boolean {
const { branchesState } = repositoryState
const { tip } = branchesState
const currentBranchName =
tip.kind === TipState.Valid ? tip.branch.name : null
const defaultBranchName =
branchesState.defaultBranch !== null
? branchesState.defaultBranch.name
: null
return (
currentBranchName !== null &&
defaultBranchName !== null &&
currentBranchName !== defaultBranchName
)
}
private hasChangedFile(repositoryState: IRepositoryState): boolean {
if (this.hasMultipleCommits(repositoryState)) {
// User has already committed a change
return true
}
const { changesState } = repositoryState
return changesState.workingDirectory.files.length > 0
}
private hasMultipleCommits(repositoryState: IRepositoryState): boolean {
const { branchesState } = repositoryState
const { tip } = branchesState
if (tip.kind === TipState.Valid) {
// For some reason sometimes the initial commit has a parent sha
// listed as an empty string...
// For now I'm filtering those out. Would be better to prevent that from happening
return tip.branch.tip.parentSHAs.some(x => x.length > 0)
}
return false
}
private commitPushed(repositoryState: IRepositoryState): boolean {
const { aheadBehind } = repositoryState
return aheadBehind !== null && aheadBehind.ahead === 0
}
private pullRequestCreated(repositoryState: IRepositoryState): boolean {
// If we see a PR at any point let's persist that. This is for the
// edge case where a user leaves the app to manually create the PR
if (repositoryState.branchesState.currentPullRequest !== null) {
this.markPullRequestTutorialStepAsComplete()
}
return this.prStepComplete
}
/** Call when the user opts to skip the install editor step */
public skipPickEditor = () => {
this.installEditorSkipped = true
setBoolean(skipInstallEditorKey, this.installEditorSkipped)
}
/**
* Call when the user has either created a pull request or opts to
* skip the create pull request step of the onboarding tutorial
*/
public markPullRequestTutorialStepAsComplete = () => {
this.prStepComplete = true
setBoolean(pullRequestStepCompleteKey, this.prStepComplete)
}
/**
* Call when a new tutorial repository is created
*
* (Resets its internal skipped steps state.)
*/
public onNewTutorialRepository = () => {
this.installEditorSkipped = false
localStorage.removeItem(skipInstallEditorKey)
this.prStepComplete = false
localStorage.removeItem(pullRequestStepCompleteKey)
this.tutorialPaused = false
localStorage.removeItem(tutorialPausedKey)
}
/** Call when the user pauses the tutorial */
public pauseTutorial() {
this.tutorialPaused = true
setBoolean(tutorialPausedKey, this.tutorialPaused)
}
/** Call when the user resumes the tutorial */
public resumeTutorial() {
this.tutorialPaused = false
setBoolean(tutorialPausedKey, this.tutorialPaused)
}
}

View file

@ -124,7 +124,8 @@ export class RepositoriesStore extends BaseStore {
repo.path,
repo.id!,
gitHubRepository,
repo.missing
repo.missing,
repo.isTutorialRepository
)
inflatedRepos.push(inflatedRepo)
}
@ -134,6 +135,53 @@ export class RepositoriesStore extends BaseStore {
)
}
/**
* Add a tutorial repository.
*
* This method differs from the `addRepository` method in that it
* requires that the repository has been created on the remote and
* set up to track it. Given that tutorial repositories are created
* from the no-repositories blank slate it shouldn't be possible for
* another repository with the same path to exist but in case that
* changes in the future this method will set the tutorial flag on
* the existing repository at the given path.
*/
public async addTutorialRepository(
path: string,
endpoint: string,
apiRepository: IAPIRepository
) {
await this.db.transaction(
'rw',
this.db.repositories,
this.db.gitHubRepositories,
this.db.owners,
async () => {
const gitHubRepository = await this.upsertGitHubRepository(
endpoint,
apiRepository
)
const existingRepo = await this.db.repositories.get({ path })
const existingRepoId =
existingRepo && existingRepo.id !== null ? existingRepo.id : undefined
return await this.db.repositories.put(
{
path,
gitHubRepositoryID: gitHubRepository.dbID,
missing: false,
lastStashCheckDate: null,
isTutorialRepository: true,
},
existingRepoId
)
}
)
this.emitUpdate()
}
/**
* Add a new local repository.
*
@ -196,20 +244,7 @@ export class RepositoriesStore extends BaseStore {
)
}
const gitHubRepositoryID = repository.gitHubRepository
? repository.gitHubRepository.dbID
: null
const oldRecord = await this.db.repositories.get(repoID)
const lastStashCheckDate =
oldRecord !== undefined ? oldRecord.lastStashCheckDate : null
await this.db.repositories.put({
id: repository.id,
path: repository.path,
missing,
gitHubRepositoryID,
lastStashCheckDate,
})
await this.db.repositories.update(repoID, { missing })
this.emitUpdate()
@ -217,7 +252,8 @@ export class RepositoriesStore extends BaseStore {
repository.path,
repository.id,
repository.gitHubRepository,
missing
missing,
repository.isTutorialRepository
)
}
@ -233,19 +269,9 @@ export class RepositoriesStore extends BaseStore {
)
}
const gitHubRepositoryID = repository.gitHubRepository
? repository.gitHubRepository.dbID
: null
const oldRecord = await this.db.repositories.get(repoID)
const lastStashCheckDate =
oldRecord !== undefined ? oldRecord.lastStashCheckDate : null
await this.db.repositories.put({
id: repository.id,
await this.db.repositories.update(repoID, {
missing: false,
path,
gitHubRepositoryID,
lastStashCheckDate,
})
this.emitUpdate()
@ -254,7 +280,8 @@ export class RepositoriesStore extends BaseStore {
path,
repository.id,
repository.gitHubRepository,
false
false,
repository.isTutorialRepository
)
}
@ -423,7 +450,8 @@ export class RepositoriesStore extends BaseStore {
repository.path,
repository.id,
updatedGitHubRepo,
repository.missing
repository.missing,
repository.isTutorialRepository
)
}

View file

@ -390,6 +390,13 @@ export class SignInStore extends TypedBaseStore<SignInState | null> {
loading: false,
error: new Error(EnterpriseTooOldMessage),
})
} else if (response.kind === AuthorizationResponseKind.WebFlowRequired) {
this.setState({
...currentState,
loading: false,
supportsBasicAuth: false,
kind: SignInStep.Authentication,
})
} else {
return assertNever(response, `Unsupported response: ${response}`)
}
@ -621,6 +628,16 @@ export class SignInStore extends TypedBaseStore<SignInState | null> {
case AuthorizationResponseKind.EnterpriseTooOld:
this.emitError(new Error(EnterpriseTooOldMessage))
break
case AuthorizationResponseKind.WebFlowRequired:
this.setState({
...currentState,
forgotPasswordUrl: this.getForgotPasswordURL(currentState.endpoint),
loading: false,
supportsBasicAuth: false,
kind: SignInStep.Authentication,
error: null,
})
break
default:
assertNever(response, `Unknown response: ${response}`)
}

View file

@ -147,6 +147,12 @@ export class Tokenizer {
maybeIssue = text.slice(index, nextIndex)
}
// handle list of issues
if (maybeIssue.endsWith(',')) {
nextIndex -= 1
maybeIssue = text.slice(index, nextIndex)
}
if (!/^#\d+$/.test(maybeIssue)) {
return null
}

View file

@ -2,6 +2,7 @@ import '../lib/logging/main/install'
import { app, Menu, ipcMain, BrowserWindow, shell } from 'electron'
import * as Fs from 'fs'
import * as URL from 'url'
import { MenuLabelsEvent } from '../models/menu-labels'
@ -72,6 +73,22 @@ function getExtraErrorContext(): Record<string, string> {
}
}
/** Extra argument for the protocol launcher on Windows */
const protocolLauncherArg = '--protocol-launcher'
const possibleProtocols = new Set(['x-github-client'])
if (__DEV__) {
possibleProtocols.add('x-github-desktop-dev-auth')
} else {
possibleProtocols.add('x-github-desktop-auth')
}
// Also support Desktop Classic's protocols.
if (__DARWIN__) {
possibleProtocols.add('github-mac')
} else if (__WIN32__) {
possibleProtocols.add('github-windows')
}
process.on('uncaughtException', (error: Error) => {
error = withSourceMappedStack(error)
reportError(error, getExtraErrorContext())
@ -189,12 +206,23 @@ function handlePossibleProtocolLauncherArgs(args: ReadonlyArray<string>) {
if (__WIN32__) {
// Desktop registers it's protocol handler callback on Windows as
// `[executable path] --protocol-launcher "%1"`. At launch it checks
// for that exact scenario here before doing any processing, and only
// processing the first argument. If there's more than 3 args because of a
// `[executable path] --protocol-launcher "%1"`. Note that extra command
// line arguments might be added by Chromium
// (https://electronjs.org/docs/api/app#event-second-instance).
// At launch Desktop checks for that exact scenario here before doing any
// processing. If there's more than one matching url argument because of a
// malformed or untrusted url then we bail out.
if (args.length === 3 && args[1] === '--protocol-launcher') {
handleAppURL(args[2])
const matchingUrls = args.filter(arg => {
const url = URL.parse(arg)
// i think this `slice` is just removing a trailing `:`
return url.protocol && possibleProtocols.has(url.protocol.slice(0, -1))
})
if (args.includes(protocolLauncherArg) && matchingUrls.length === 1) {
handleAppURL(matchingUrls[0])
} else {
log.error(`Malformed launch arguments received: ${args}`)
}
} else if (args.length > 1) {
handleAppURL(args[1])
@ -208,7 +236,7 @@ function handlePossibleProtocolLauncherArgs(args: ReadonlyArray<string>) {
function setAsDefaultProtocolClient(protocol: string) {
if (__WIN32__) {
app.setAsDefaultProtocolClient(protocol, process.execPath, [
'--protocol-launcher',
protocolLauncherArg,
])
} else {
app.setAsDefaultProtocolClient(protocol)
@ -229,20 +257,7 @@ app.on('ready', () => {
readyTime = now() - launchTime
setAsDefaultProtocolClient('x-github-client')
if (__DEV__) {
setAsDefaultProtocolClient('x-github-desktop-dev-auth')
} else {
setAsDefaultProtocolClient('x-github-desktop-auth')
}
// Also support Desktop Classic's protocols.
if (__DARWIN__) {
setAsDefaultProtocolClient('github-mac')
} else if (__WIN32__) {
setAsDefaultProtocolClient('github-windows')
}
possibleProtocols.forEach(protocol => setAsDefaultProtocolClient(protocol))
createWindow()
@ -471,7 +486,7 @@ app.on('ready', () => {
ipcMain.on(
'open-external',
(event: Electron.IpcMessageEvent, { path }: { path: string }) => {
async (event: Electron.IpcMessageEvent, { path }: { path: string }) => {
const pathLowerCase = path.toLowerCase()
if (
pathLowerCase.startsWith('http://') ||
@ -480,7 +495,14 @@ app.on('ready', () => {
log.info(`opening in browser: ${path}`)
}
const result = shell.openExternal(path)
let result
try {
await shell.openExternal(path)
result = true
} catch (e) {
log.error(`Call to openExternal failed: '${e}'`)
result = false
}
event.sender.send('open-external-result', { result })
}
)

View file

@ -4,8 +4,6 @@ import { MenuEvent } from './menu-event'
import { truncateWithEllipsis } from '../../lib/truncate-with-ellipsis'
import { getLogDirectoryPath } from '../../lib/logging/get-log-path'
import { ensureDir } from 'fs-extra'
import { log } from '../log'
import { openDirectorySafe } from '../shell'
import { enableRebaseDialog, enableStashing } from '../../lib/feature-flag'
import { MenuLabelsEvent } from '../../models/menu-labels'
@ -425,32 +423,40 @@ export function buildDefaultMenu({
const submitIssueItem: Electron.MenuItemConstructorOptions = {
label: __DARWIN__ ? 'Report Issue…' : 'Report issue…',
click() {
shell.openExternal('https://github.com/desktop/desktop/issues/new/choose')
shell
.openExternal('https://github.com/desktop/desktop/issues/new/choose')
.catch(err => log.error('Failed opening issue creation page', err))
},
}
const contactSupportItem: Electron.MenuItemConstructorOptions = {
label: __DARWIN__ ? 'Contact GitHub Support…' : '&Contact GitHub support…',
click() {
shell.openExternal(
`https://github.com/contact?from_desktop_app=1&app_version=${app.getVersion()}`
)
shell
.openExternal(
`https://github.com/contact?from_desktop_app=1&app_version=${app.getVersion()}`
)
.catch(err => log.error('Failed opening contact support page', err))
},
}
const showUserGuides: Electron.MenuItemConstructorOptions = {
label: 'Show User Guides',
click() {
shell.openExternal('https://help.github.com/desktop/guides/')
shell
.openExternal('https://help.github.com/desktop/guides/')
.catch(err => log.error('Failed opening user guides page', err))
},
}
const showKeyboardShortcuts: Electron.MenuItemConstructorOptions = {
label: __DARWIN__ ? 'Show Keyboard Shortcuts' : 'Show keyboard shortcuts',
click() {
shell.openExternal(
'https://help.github.com/en/desktop/getting-started-with-github-desktop/keyboard-shortcuts-in-github-desktop'
)
shell
.openExternal(
'https://help.github.com/en/desktop/getting-started-with-github-desktop/keyboard-shortcuts-in-github-desktop'
)
.catch(err => log.error('Failed opening keyboard shortcuts page', err))
},
}
@ -469,7 +475,7 @@ export function buildDefaultMenu({
openDirectorySafe(logPath)
})
.catch(err => {
log('error', err.message)
log.error('Failed opening logs directory', err)
})
},
}

View file

@ -18,7 +18,9 @@ export function openDirectorySafe(path: string) {
slashes: true,
})
shell.openExternal(directoryURL)
shell
.openExternal(directoryURL)
.catch(err => log.error(`Failed to open directory (${path})`, err))
} else {
shell.openItem(path)
}

View file

@ -19,7 +19,7 @@ export class Account {
* @param token The access token used to perform operations on behalf of this account
* @param emails The current list of email addresses associated with the account
* @param avatarURL The profile URL to render for this account
* @param id The database id for this account
* @param id The GitHub.com or GitHub Enterprise Server database id for this account.
* @param name The friendly name associated with this account
*/
public constructor(

View file

@ -27,7 +27,7 @@ export enum StartPoint {
}
/**
* Check if a branch is eligible for beign fast forarded.
* Check if a branch is eligible for being fast-forwarded.
*
* Requirements:
* 1. It's local.

View file

@ -8,6 +8,7 @@ import { WorkingDirectoryFileChange } from './status'
import { PreferencesTab } from './preferences'
import { ICommitContext } from './commit'
import { IStashEntry } from './stash-entry'
import { Account } from '../models/account'
export enum PopupType {
RenameBranch = 1,
@ -49,6 +50,9 @@ export enum PopupType {
StashAndSwitchBranch,
ConfirmOverwriteStash,
ConfirmDiscardStash,
CreateTutorialRepository,
ConfirmExitTutorial,
PushRejectedDueToMissingWorkflowScope,
}
export type Popup =
@ -195,3 +199,15 @@ export type Popup =
repository: Repository
stash: IStashEntry
}
| {
type: PopupType.CreateTutorialRepository
account: Account
}
| {
type: PopupType.ConfirmExitTutorial
}
| {
type: PopupType.PushRejectedDueToMissingWorkflowScope
rejectedPath: string
repository: Repository
}

View file

@ -2,6 +2,7 @@ import * as Path from 'path'
import { GitHubRepository } from './github-repository'
import { IAheadBehind } from './branch'
import { enableTutorial } from '../lib/feature-flag'
function getBaseName(path: string): string {
const baseName = Path.basename(path)
@ -37,7 +38,8 @@ export class Repository {
path: string,
public readonly id: number,
public readonly gitHubRepository: GitHubRepository | null,
public readonly missing: boolean
public readonly missing: boolean,
private readonly _isTutorialRepository?: boolean
) {
this.mainWorkTree = { path }
this.name = (gitHubRepository && gitHubRepository.name) || getBaseName(path)
@ -55,7 +57,17 @@ export class Repository {
public get hash(): string {
return `${this.id}+${this.gitHubRepository && this.gitHubRepository.hash}+${
this.path
}+${this.missing}+${this.name}`
}+${this.missing}+${this.name}+${this.isTutorialRepository}`
}
/**
* True if the repository is a tutorial repository created as part
* of the onboarding flow. Tutorial repositories trigger a tutorial
* user experience which introduces new users to some core concepts
* of Git and GitHub.
*/
public get isTutorialRepository() {
return enableTutorial() && this._isTutorialRepository === true
}
}

View file

@ -0,0 +1,36 @@
export enum TutorialStep {
NotApplicable = 'NotApplicable',
PickEditor = 'PickEditor',
CreateBranch = 'CreateBranch',
EditFile = 'EditFile',
MakeCommit = 'MakeCommit',
PushBranch = 'PushBranch',
OpenPullRequest = 'OpenPullRequest',
AllDone = 'AllDone',
Paused = 'Paused',
}
export type ValidTutorialStep =
| TutorialStep.PickEditor
| TutorialStep.CreateBranch
| TutorialStep.EditFile
| TutorialStep.MakeCommit
| TutorialStep.PushBranch
| TutorialStep.OpenPullRequest
| TutorialStep.AllDone
export function isValidTutorialStep(
step: TutorialStep
): step is ValidTutorialStep {
return step !== TutorialStep.NotApplicable && step !== TutorialStep.Paused
}
export const orderedTutorialSteps: ReadonlyArray<ValidTutorialStep> = [
TutorialStep.PickEditor,
TutorialStep.CreateBranch,
TutorialStep.EditFile,
TutorialStep.MakeCommit,
TutorialStep.PushBranch,
TutorialStep.OpenPullRequest,
TutorialStep.AllDone,
]

View file

@ -1,5 +1,4 @@
import * as React from 'react'
import { clipboard } from 'electron'
import { Row } from '../lib/row'
import { Button } from '../lib/button'
@ -70,10 +69,6 @@ export class About extends React.Component<IAboutProps, IAboutState> {
this.setState({ updateState })
}
private onClickVersion = () => {
clipboard.writeText(this.props.applicationVersion)
}
public componentDidMount() {
this.updateStoreEventHandle = updateStore.onDidChange(
this.onUpdateStateChanged
@ -270,14 +265,8 @@ export class About extends React.Component<IAboutProps, IAboutState> {
</Row>
<h2>{name}</h2>
<p className="no-padding">
<LinkButton
title="Click to copy"
className="version-text"
onClick={this.onClickVersion}
>
{versionText}
</LinkButton>{' '}
({releaseNotesLink})
<span className="selectable-text">{versionText}</span> (
{releaseNotesLink})
</p>
<p className="no-padding">
<LinkButton onClick={this.props.onShowTermsAndConditions}>

View file

@ -17,7 +17,7 @@ import { updateStore, UpdateStatus } from './lib/update-store'
import { RetryAction } from '../models/retry-actions'
import { shouldRenderApplicationMenu } from './lib/features'
import { matchExistingRepository } from '../lib/repository-matching'
import { getDotComAPIEndpoint } from '../lib/api'
import { getDotComAPIEndpoint, IAPIRepository } from '../lib/api'
import { ILaunchStats, SamplesURL } from '../lib/stats'
import { getVersion, getName } from './lib/app-proxy'
import { getOS } from '../lib/get-os'
@ -103,6 +103,11 @@ import { BannerType } from '../models/banner'
import { StashAndSwitchBranch } from './stash-changes/stash-and-switch-branch-dialog'
import { OverwriteStash } from './stash-changes/overwrite-stashed-changes-dialog'
import { ConfirmDiscardStashDialog } from './stashing/confirm-discard-stash'
import { CreateTutorialRepositoryDialog } from './blank-slate/create-tutorial-repository-dialog'
import { enableTutorial } from '../lib/feature-flag'
import { ConfirmExitTutorial } from './tutorial'
import { TutorialStep, isValidTutorialStep } from '../models/tutorial-step'
import { WorkflowPushRejectedDialog } from './workflow-push-rejected/workflow-push-rejected'
const MinuteInMilliseconds = 1000 * 60
const HourInMilliseconds = MinuteInMilliseconds * 60
@ -679,6 +684,47 @@ export class App extends React.Component<IAppProps, IAppState> {
})
}
private onCreateTutorialRepository = () => {
if (!enableTutorial()) {
return
}
const account = this.getDotComAccount() || this.getEnterpriseAccount()
if (account === null) {
return
}
this.props.dispatcher.showPopup({
type: PopupType.CreateTutorialRepository,
account,
})
}
private onResumeTutorialRepository = () => {
const tutorialRepository = this.getSelectedTutorialRepository()
if (!tutorialRepository) {
return
}
this.props.dispatcher.resumeTutorial(tutorialRepository)
}
private getSelectedTutorialRepository() {
const { selectedState } = this.state
const selectedRepository =
selectedState && selectedState.type === SelectionType.Repository
? selectedState.repository
: null
const isTutorialRepository =
enableTutorial() &&
selectedRepository &&
selectedRepository.isTutorialRepository
return isTutorialRepository ? selectedRepository : null
}
private showAbout() {
this.props.dispatcher.showPopup({ type: PopupType.About })
}
@ -944,7 +990,7 @@ export class App extends React.Component<IAppProps, IAppState> {
// it if needed.
if (paths.length > 1) {
const addedRepositories = await this.addRepositories(paths)
if (addedRepositories.length) {
if (addedRepositories.length > 0) {
this.props.dispatcher.recordAddExistingRepository()
}
} else {
@ -1010,7 +1056,7 @@ export class App extends React.Component<IAppProps, IAppState> {
private async addRepositories(paths: ReadonlyArray<string>) {
const repositories = await this.props.dispatcher.addRepositories(paths)
if (repositories.length) {
if (repositories.length > 0) {
this.props.dispatcher.selectRepository(repositories[0])
}
@ -1110,11 +1156,6 @@ export class App extends React.Component<IAppProps, IAppState> {
return null
}
// Don't render the menu bar when the blank slate is shown
if (this.state.repositories.length < 1) {
return null
}
const currentFoldout = this.state.currentFoldout
// AppMenuBar requires us to pass a strongly typed AppMenuFoldout state or
@ -1162,11 +1203,23 @@ export class App extends React.Component<IAppProps, IAppState> {
}
const showAppIcon = __WIN32__ && !this.state.showWelcomeFlow
const inWelcomeFlow = this.state.showWelcomeFlow
const inNoRepositoriesView = this.inNoRepositoriesBlankSlateState()
// The light title bar style should only be used while we're in
// the welcome flow as well as the no-repositories blank slate
// on macOS. The latter case has to do with the application menu
// being part of the title bar on Windows. We need to render
// the app menu in the no-repositories blank slate on Windows but
// the menu doesn't support the light style at the moment so we're
// forcing it to use the dark style.
const titleBarStyle =
inWelcomeFlow || (__DARWIN__ && inNoRepositoriesView) ? 'light' : 'dark'
return (
<TitleBar
showAppIcon={showAppIcon}
titleBarStyle={this.state.titleBarStyle}
titleBarStyle={titleBarStyle}
windowState={this.state.windowState}
windowZoomFactor={this.state.windowZoomFactor}
>
@ -1774,11 +1827,68 @@ export class App extends React.Component<IAppProps, IAppState> {
/>
)
}
case PopupType.CreateTutorialRepository: {
return (
<CreateTutorialRepositoryDialog
key="create-tutorial-repository-dialog"
dispatcher={this.props.dispatcher}
account={popup.account}
onDismissed={this.onPopupDismissed}
onTutorialRepositoryCreated={this.onTutorialRepositoryCreated}
onError={this.onTutorialRepositoryError}
/>
)
}
case PopupType.ConfirmExitTutorial: {
return (
<ConfirmExitTutorial
key="confirm-exit-tutorial"
onDismissed={this.onPopupDismissed}
onContinue={this.onExitTutorialToHomeScreen}
/>
)
}
case PopupType.PushRejectedDueToMissingWorkflowScope:
return (
<WorkflowPushRejectedDialog
onDismissed={this.onPopupDismissed}
rejectedPath={popup.rejectedPath}
dispatcher={this.props.dispatcher}
repository={popup.repository}
/>
)
default:
return assertNever(popup, `Unknown popup type: ${popup}`)
}
}
private onExitTutorialToHomeScreen = () => {
const tutorialRepository = this.getSelectedTutorialRepository()
if (!tutorialRepository) {
return
}
this.props.dispatcher.pauseTutorial(tutorialRepository)
this.props.dispatcher.closePopup()
}
private onTutorialRepositoryError = (error: Error) => {
this.props.dispatcher.closePopup(PopupType.CreateTutorialRepository)
this.props.dispatcher.postError(error)
}
private onTutorialRepositoryCreated = (
path: string,
account: Account,
apiRepository: IAPIRepository
) => {
return this.props.dispatcher.addTutorialRepository(
path,
account.endpoint,
apiRepository
)
}
private onShowRebaseConflictsBanner = (
repository: Repository,
targetBranch: string
@ -2017,6 +2127,22 @@ export class App extends React.Component<IAppProps, IAppState> {
}
}
private onExitTutorial = () => {
if (
this.state.repositories.length === 1 &&
isValidTutorialStep(this.state.currentOnboardingTutorialStep)
) {
// If the only repository present is the tutorial repo,
// prompt for confirmation and exit to the BlankSlateView
this.props.dispatcher.showPopup({
type: PopupType.ConfirmExitTutorial,
})
} else {
// Otherwise pop open repositories panel
this.onRepositoryDropdownStateChanged('open')
}
}
private renderRepositoryToolbarButton() {
const selection = this.state.selectedState
@ -2211,7 +2337,7 @@ export class App extends React.Component<IAppProps, IAppState> {
// can't support banners at the moment. So for the
// no-repositories blank slate we'll have to live without
// them.
if (this.state.repositories.length === 0) {
if (this.inNoRepositoriesBlankSlateState()) {
return null
}
@ -2256,7 +2382,7 @@ export class App extends React.Component<IAppProps, IAppState> {
/**
* No toolbar if we're in the blank slate view.
*/
if (this.state.repositories.length === 0) {
if (this.inNoRepositoriesBlankSlateState()) {
return null
}
@ -2276,7 +2402,7 @@ export class App extends React.Component<IAppProps, IAppState> {
private renderRepository() {
const state = this.state
if (state.repositories.length < 1) {
if (this.inNoRepositoriesBlankSlateState()) {
return (
<BlankSlateView
dotComAccount={this.getDotComAccount()}
@ -2284,7 +2410,10 @@ export class App extends React.Component<IAppProps, IAppState> {
onCreate={this.showCreateRepository}
onClone={this.showCloneRepo}
onAdd={this.showAddLocalRepo}
apiRepositories={this.state.apiRepositories}
onCreateTutorialRepository={this.onCreateTutorialRepository}
onResumeTutorialRepository={this.onResumeTutorialRepository}
tutorialPaused={this.isTutorialPaused()}
apiRepositories={state.apiRepositories}
onRefreshRepositories={this.onRefreshRepositories}
/>
)
@ -2318,8 +2447,11 @@ export class App extends React.Component<IAppProps, IAppState> {
}
accounts={state.accounts}
externalEditorLabel={externalEditorLabel}
resolvedExternalEditor={state.resolvedExternalEditor}
onOpenInExternalEditor={this.openFileInExternalEditor}
appMenu={this.state.appMenuState[0]}
appMenu={state.appMenuState[0]}
currentTutorialStep={state.currentOnboardingTutorialStep}
onExitTutorial={this.onExitTutorial}
/>
)
} else if (selectedState.type === SelectionType.CloningRepository) {
@ -2412,6 +2544,14 @@ export class App extends React.Component<IAppProps, IAppState> {
kind: HistoryTabMode.History,
})
}
private inNoRepositoriesBlankSlateState() {
return this.state.repositories.length === 0 || this.isTutorialPaused()
}
private isTutorialPaused() {
return this.state.currentOnboardingTutorialStep === TutorialStep.Paused
}
}
function NoRepositorySelected() {

View file

@ -13,6 +13,7 @@ import { CloneableRepositoryFilterList } from '../clone-repository/cloneable-rep
import { IAPIRepository } from '../../lib/api'
import { assertNever } from '../../lib/fatal-error'
import { ClickSource } from '../lib/list'
import { enableTutorial } from '../../lib/feature-flag'
interface IBlankSlateProps {
/** A function to call when the user chooses to create a repository. */
@ -24,6 +25,15 @@ interface IBlankSlateProps {
/** A function to call when the user chooses to add a local repository. */
readonly onAdd: () => void
/** Called when the user chooses to create a tutorial repository */
readonly onCreateTutorialRepository: () => void
/** Called when the user chooses to resume a tutorial repository */
readonly onResumeTutorialRepository: () => void
/** true if tutorial is in paused state. */
readonly tutorialPaused: boolean
/** The logged in account for GitHub.com. */
readonly dotComAccount: Account | null
@ -321,42 +331,100 @@ export class BlankSlateView extends React.Component<
}
}
// Note: this wrapper is necessary in order to ensure
// `onClone` does not get passed a click event
// and accidentally interpret that as a url
// See https://github.com/desktop/desktop/issues/8394
private onShowClone = () => this.props.onClone()
private renderButtonGroupButton(
symbol: OcticonSymbol,
title: string,
onClick: () => void,
type?: 'submit'
) {
return (
<li>
<Button onClick={onClick} type={type}>
<Octicon symbol={symbol} />
<div>{title}</div>
</Button>
</li>
)
}
private renderTutorialRepositoryButton() {
if (!enableTutorial()) {
return null
}
// No tutorial if you're not signed in.
if (
this.props.dotComAccount === null &&
this.props.enterpriseAccount === null
) {
return null
}
if (this.props.tutorialPaused) {
return this.renderButtonGroupButton(
OcticonSymbol.mortarBoard,
__DARWIN__
? 'Return to In Progress Tutorial'
: 'Return to in progress tutorial',
this.props.onResumeTutorialRepository,
'submit'
)
} else {
return this.renderButtonGroupButton(
OcticonSymbol.mortarBoard,
__DARWIN__
? 'Create a Tutorial Repository…'
: 'Create a tutorial repository…',
this.props.onCreateTutorialRepository,
'submit'
)
}
}
private renderCloneButton() {
return this.renderButtonGroupButton(
OcticonSymbol.repoClone,
__DARWIN__
? 'Clone a Repository from the Internet…'
: 'Clone a repository from the Internet…',
this.onShowClone
)
}
private renderCreateRepositoryButton() {
return this.renderButtonGroupButton(
OcticonSymbol.plus,
__DARWIN__
? 'Create a New Repository on your Hard Drive…'
: 'Create a New Repository on your hard drive…',
this.props.onCreate
)
}
private renderAddExistingRepositoryButton() {
return this.renderButtonGroupButton(
OcticonSymbol.fileDirectory,
__DARWIN__
? 'Add an Existing Repository from your Hard Drive…'
: 'Add an Existing Repository from your hard drive…',
this.props.onAdd
)
}
private renderRightPanel() {
return (
<div className="content-pane right">
<ul className="button-group">
<li>
<Button onClick={this.onShowClone}>
<Octicon symbol={OcticonSymbol.repoClone} />
<div>
{__DARWIN__
? 'Clone a Repository from the Internet…'
: 'Clone a repository from the Internet…'}
</div>
</Button>
</li>
<li>
<Button onClick={this.props.onCreate}>
<Octicon symbol={OcticonSymbol.plus} />
<div>
{__DARWIN__
? 'Create a New Repository on your Hard Drive…'
: 'Create a New Repository on your hard drive…'}
</div>
</Button>
</li>
<li>
<Button onClick={this.props.onAdd}>
<Octicon symbol={OcticonSymbol.fileDirectory} />
<div>
{__DARWIN__
? 'Add an Existing Repository from your Hard Drive…'
: 'Add an Existing Repository from your hard drive…'}
</div>
</Button>
</li>
{this.renderTutorialRepositoryButton()}
{this.renderCloneButton()}
{this.renderCreateRepositoryButton()}
{this.renderAddExistingRepositoryButton()}
</ul>
<div className="drag-drop-info">

View file

@ -0,0 +1,306 @@
import * as React from 'react'
import * as URL from 'url'
import * as Path from 'path'
import { Dialog, DialogContent, DialogFooter } from '../dialog'
import { Button } from '../lib/button'
import { ButtonGroup } from '../lib/button-group'
import { Account } from '../../models/account'
import {
getDotComAPIEndpoint,
getHTMLURL,
API,
IAPIRepository,
} from '../../lib/api'
import { Ref } from '../lib/ref'
import { LinkButton } from '../lib/link-button'
import { getDefaultDir } from '../lib/default-dir'
import { writeFile, pathExists, ensureDir } from 'fs-extra'
import { git, GitError } from '../../lib/git'
import { envForAuthentication } from '../../lib/git/authentication'
import {
PushProgressParser,
executionOptionsWithProgress,
} from '../../lib/progress'
import { Progress } from '../../models/progress'
import { Dispatcher } from '../dispatcher'
import { APIError } from '../../lib/http'
interface ICreateTutorialRepositoryDialogProps {
/**
* The GitHub.com, or GitHub Enterprise Server account that will
* be the owner of the tutorial repository.
*/
readonly account: Account
readonly dispatcher: Dispatcher
/**
* Event triggered when the dialog is dismissed by the user in the
* ways described in the Dialog component's dismissable prop.
*/
readonly onDismissed: () => void
/**
* Event triggered when the tutorial repository has been created
* locally, initialized with the expected tutorial contents, and
* pushed to the remote.
*
* @param path The path on the local machine where the tutorial
* repository was created
*
* @param account The account (and thereby the GitHub host) under
* which the repository was created
*
* @param apiRepository The repository information as returned by
* the GitHub API as the repository was created.
*/
readonly onTutorialRepositoryCreated: (
path: string,
account: Account,
apiRepository: IAPIRepository
) => Promise<void>
/**
* Event triggered when the component encounters an error while
* attempting to create the tutorial repository. Consumers are
* intended to display an error message to the end user in response
* to this event.
*/
readonly onError: (error: Error) => void
}
interface ICreateTutorialRepositoryDialogState {
/**
* Whether or not the dialog is currently in the process of creating
* the tutorial repository. When true this will render a spinning
* progress icon in the dialog header (if the dialog has a header) as
* well as temporarily disable dismissal of the dialog.
*/
readonly loading: boolean
/**
* The current progress in creating the tutorial repository. Undefined
* until the creation process starts.
*/
readonly progress?: Progress
}
const nl = __WIN32__ ? '\r\n' : '\n'
const InititalReadmeContents =
`# Welcome to GitHub Desktop!${nl}${nl}` +
`This is your README. READMEs are where you can communicate ` +
`what your project is and how to use it.${nl}${nl}` +
`Write your name on line 6, save it, and then head ` +
`back to GitHub Desktop.${nl}`
/**
* A dialog component reponsible for initializing, publishing, and adding
* a tutorial repository to the application.
*/
export class CreateTutorialRepositoryDialog extends React.Component<
ICreateTutorialRepositoryDialogProps,
ICreateTutorialRepositoryDialogState
> {
public constructor(props: ICreateTutorialRepositoryDialogProps) {
super(props)
this.state = { loading: false }
}
private async createAPIRepository(account: Account, name: string) {
const api = new API(account.endpoint, account.token)
try {
return await api.createRepository(
null,
name,
'GitHub Desktop tutorial repository',
true
)
} catch (err) {
if (
err instanceof APIError &&
err.responseStatus === 422 &&
err.apiError !== null
) {
if (err.apiError.message === 'Repository creation failed.') {
if (
err.apiError.errors &&
err.apiError.errors.some(
x => x.message === 'name already exists on this account'
)
) {
throw new Error(
'You already have a repository named ' +
`"${name}" on your account at ${friendlyEndpointName(
account
)}.\n\n` +
'Please delete the repository and try again.'
)
}
}
}
throw err
}
}
private async pushRepo(
path: string,
account: Account,
progressCb: (title: string, value: number, description?: string) => void
) {
const pushTitle = `Pushing repository to ${friendlyEndpointName(account)}`
progressCb(pushTitle, 0)
const pushOpts = await executionOptionsWithProgress(
{
env: envForAuthentication(account),
},
new PushProgressParser(),
progress => {
if (progress.kind === 'progress') {
progressCb(pushTitle, progress.percent, progress.details.text)
}
}
)
const args = ['push', '-u', 'origin', 'master']
await git(args, path, 'tutorial:push', pushOpts)
}
public onSubmit = async () => {
this.props.dispatcher.recordTutorialStarted()
const { account } = this.props
const endpointName = friendlyEndpointName(account)
this.setState({ loading: true })
const name = 'desktop-tutorial'
try {
const path = Path.resolve(getDefaultDir(), name)
if (await pathExists(path)) {
throw new Error(
`The path ${path} already exists. Please move it ` +
'out of the way, or remove it, and then try again.'
)
}
this.setProgress(`Creating repository on ${endpointName}`, 0)
const repo = await this.createAPIRepository(account, name)
this.setProgress('Initializing local repository', 0.2)
await ensureDir(path)
await git(['init'], path, 'tutorial:init')
await writeFile(Path.join(path, 'README.md'), InititalReadmeContents)
await git(['add', '--', 'README.md'], path, 'tutorial:add')
await git(
['commit', '-m', 'Initial commit', '--', 'README.md'],
path,
'tutorial:commit'
)
await git(
['remote', 'add', 'origin', repo.clone_url],
path,
'tutorial:add-remote'
)
await this.pushRepo(path, account, (title, value, description) => {
this.setProgress(title, 0.3 + value * 0.6, description)
})
this.setProgress('Finalizing tutorial repository', 0.9)
await this.props.onTutorialRepositoryCreated(path, account, repo)
this.props.dispatcher.recordTutorialRepositoryCreated()
this.props.onDismissed()
} catch (err) {
this.setState({ loading: false, progress: undefined })
if (err instanceof GitError) {
this.props.onError(err)
} else {
this.props.onError(
new Error(
`Failed creating the tutorial repository.\n\n${err.message}`
)
)
}
}
}
private setProgress(title: string, value: number, description?: string) {
this.setState({
progress: { kind: 'generic', title, value, description },
})
}
public onCancel = () => {
this.props.onDismissed()
}
private renderProgress() {
if (this.state.progress === undefined) {
return null
}
const { progress } = this.state
const description = progress.description ? (
<div className="description">{progress.description}</div>
) : null
return (
<div className="progress-container">
<div>{progress.title}</div>
<progress value={progress.value} />
{description}
</div>
)
}
public render() {
const { account } = this.props
return (
<Dialog
id="create-tutorial-repository-dialog"
title="Start tutorial"
onDismissed={this.onCancel}
onSubmit={this.onSubmit}
dismissable={!this.state.loading}
loading={this.state.loading}
disabled={this.state.loading}
>
<DialogContent>
<div>
This will create a repository on your local machine, and push it to
your account <Ref>@{this.props.account.login}</Ref> on{' '}
<LinkButton uri={getHTMLURL(account.endpoint)}>
{friendlyEndpointName(account)}
</LinkButton>
. This repository will only be visible to you, and not visible
publicly.
</div>
{this.renderProgress()}
</DialogContent>
<DialogFooter>
<ButtonGroup>
<Button type="submit">Continue</Button>
<Button onClick={this.onCancel}>Cancel</Button>
</ButtonGroup>
</DialogFooter>
</Dialog>
)
}
}
function friendlyEndpointName(account: Account) {
return account.endpoint === getDotComAPIEndpoint()
? 'GitHub.com'
: URL.parse(account.endpoint).hostname || account.endpoint
}

View file

@ -530,9 +530,9 @@ export class ChangesList extends React.Component<
private getPlaceholderMessage(
files: ReadonlyArray<WorkingDirectoryFileChange>,
singleFileCommit: boolean
prepopulateCommitSummary: boolean
) {
if (!singleFileCommit) {
if (!prepopulateCommitSummary) {
return 'Summary (required)'
}
@ -598,7 +598,13 @@ export class ChangesList extends React.Component<
const filesSelected = workingDirectory.files.filter(
f => f.selection.getSelectionType() !== DiffSelectionType.None
)
const singleFileCommit = filesSelected.length === 1
// When a single file is selected, we use a default commit summary
// based on the file name and change status.
// However, for onboarding tutorial repositories, we don't want to do this.
// See https://github.com/desktop/desktop/issues/8354
const prepopulateCommitSummary =
filesSelected.length === 1 && !repository.isTutorialRepository
return (
<CommitMessage
@ -617,9 +623,9 @@ export class ChangesList extends React.Component<
coAuthors={this.props.coAuthors}
placeholder={this.getPlaceholderMessage(
filesSelected,
singleFileCommit
prepopulateCommitSummary
)}
singleFileCommit={singleFileCommit}
prepopulateCommitSummary={prepopulateCommitSummary}
key={repository.id}
currentBranchProtected={currentBranchProtected}
/>

View file

@ -48,7 +48,7 @@ interface ICommitMessageProps {
readonly autocompletionProviders: ReadonlyArray<IAutocompletionProvider<any>>
readonly isCommitting: boolean
readonly placeholder: string
readonly singleFileCommit: boolean
readonly prepopulateCommitSummary: boolean
readonly currentBranchProtected: boolean
/**
@ -211,7 +211,7 @@ export class CommitMessage extends React.Component<
const trailers = this.getCoAuthorTrailers()
const summaryOrPlaceholder =
this.props.singleFileCommit && !this.state.summary
this.props.prepopulateCommitSummary && !this.state.summary
? this.props.placeholder
: summary
@ -233,7 +233,7 @@ export class CommitMessage extends React.Component<
private canCommit(): boolean {
return (
(this.props.anyFilesSelected && this.state.summary.length > 0) ||
this.props.singleFileCommit
this.props.prepopulateCommitSummary
)
}
@ -399,7 +399,7 @@ export class CommitMessage extends React.Component<
private onDescriptionTextAreaRef = (elem: HTMLTextAreaElement | null) => {
if (elem) {
elem.addEventListener('scroll', () => {
const checkDescriptionScrollState = () => {
if (this.descriptionTextAreaScrollDebounceId !== null) {
cancelAnimationFrame(this.descriptionTextAreaScrollDebounceId)
this.descriptionTextAreaScrollDebounceId = null
@ -407,7 +407,9 @@ export class CommitMessage extends React.Component<
this.descriptionTextAreaScrollDebounceId = requestAnimationFrame(
this.onDescriptionTextAreaScroll
)
})
}
elem.addEventListener('input', checkDescriptionScrollState)
elem.addEventListener('scroll', checkDescriptionScrollState)
}
this.descriptionTextArea = elem

View file

@ -382,7 +382,7 @@ export class ChangesSidebar extends React.Component<IChangesSidebarProps, {}> {
const isShowingStashEntry = selection.kind === ChangesSelectionKind.Stash
return (
<div id="changes-sidebar-contents">
<div className="panel">
<ChangesList
dispatcher={this.props.dispatcher}
repository={this.props.repository}

View file

@ -202,9 +202,21 @@ export class Dialog extends React.Component<IDialogProps, IDialogState> {
}
public componentDidMount() {
if (!this.dialogElement) {
return
}
// This cast to any is necessary since React doesn't know about the
// dialog element yet.
;(this.dialogElement as any).showModal()
// Provide an event that components can subscribe to in order to perform
// tasks such as re-layout after the dialog is visible
this.dialogElement.dispatchEvent(
new CustomEvent('dialog-show', {
bubbles: true,
cancelable: false,
})
)
this.setState({ isAppearing: true })
this.scheduleDismissGraceTimeout()

View file

@ -2,7 +2,7 @@ import { remote } from 'electron'
import { Disposable, IDisposable } from 'event-kit'
import * as Path from 'path'
import { IAPIOrganization, IAPIRefStatus } from '../../lib/api'
import { IAPIOrganization, IAPIRefStatus, IAPIRepository } from '../../lib/api'
import { shell } from '../../lib/app-shell'
import {
CompareAction,
@ -126,6 +126,35 @@ export class Dispatcher {
return this.appStore._addRepositories(paths)
}
/**
* Add a tutorial repository.
*
* This method differs from the `addRepositories` method in that it
* requires that the repository has been created on the remote and
* set up to track it. Given that tutorial repositories are created
* from the no-repositories blank slate it shouldn't be possible for
* another repository with the same path to exist but in case that
* changes in the future this method will set the tutorial flag on
* the existing repository at the given path.
*/
public addTutorialRepository(
path: string,
endpoint: string,
apiRepository: IAPIRepository
) {
return this.appStore._addTutorialRepository(path, endpoint, apiRepository)
}
/** Resume an already started onboarding tutorial */
public resumeTutorial(repository: Repository) {
return this.appStore._resumeTutorial(repository)
}
/** Suspend the onboarding tutorial and go to the no repositories blank slate view */
public pauseTutorial(repository: Repository) {
return this.appStore._pauseTutorial(repository)
}
/** Remove the repositories represented by the given IDs from local storage. */
public removeRepositories(
repositories: ReadonlyArray<Repository | CloningRepository>,
@ -717,11 +746,6 @@ export class Dispatcher {
return this.appStore._setCommitMessage(repository, message)
}
/** Add the account to the app. */
public addAccount(account: Account): Promise<void> {
return this.appStore._addAccount(account)
}
/** Remove the given account from the app. */
public removeAccount(account: Account): Promise<void> {
return this.appStore._removeAccount(account)
@ -1674,6 +1698,13 @@ export class Dispatcher {
return this.appStore._changeBranchesTab(tab)
}
/**
* Open the Explore page at the GitHub instance of this repository
*/
public showGitHubExplore(repository: Repository): Promise<void> {
return this.appStore._showGitHubExplore(repository)
}
/**
* Open the Create Pull Request page on GitHub after verifying ahead/behind.
*
@ -1778,10 +1809,6 @@ export class Dispatcher {
return this.appStore._updateCompareForm(repository, newState)
}
public resolveCurrentEditor() {
return this.appStore._resolveCurrentEditor()
}
/**
* update the manual resolution method for a file
*/
@ -2154,4 +2181,30 @@ export class Dispatcher {
public recordStashView(): Promise<void> {
return this.statsStore.recordStashView()
}
/** Call when the user opts to skip the pick editor step of the onboarding tutorial */
public skipPickEditorTutorialStep(repository: Repository) {
return this.appStore._skipPickEditorTutorialStep(repository)
}
/**
* Call when the user has either created a pull request or opts to
* skip the create pull request step of the onboarding tutorial
*/
public markPullRequestTutorialStepAsComplete(repository: Repository) {
return this.appStore._markPullRequestTutorialStepAsComplete(repository)
}
/**
* Onboarding tutorial has been started
*/
public recordTutorialStarted() {
return this.statsStore.recordTutorialStarted()
}
/**
* Onboarding tutorial has been successfully created
*/
public recordTutorialRepositoryCreated() {
return this.statsStore.recordTutorialRepoCreated()
}
}

View file

@ -13,6 +13,7 @@ import { UpstreamAlreadyExistsError } from '../../lib/stores/upstream-already-ex
import { PopupType } from '../../models/popup'
import { Repository } from '../../models/repository'
import { getDotComAPIEndpoint } from '../../lib/api'
/** An error which also has a code property. */
interface IErrorWithCode extends Error {
@ -462,3 +463,61 @@ export async function localChangesOverwrittenHandler(
return null
}
const rejectedPathRe = /^ ! \[remote rejected\] .*? -> .*? \(refusing to allow an integration to create or update (.*?)\)$/m
/**
* Attempts to detect whether an error is the result of a failed push
* due to insufficient OAuth permissions (missing workflow scope)
*/
export async function refusedWorkflowUpdate(
error: Error,
dispatcher: Dispatcher
) {
const e = asErrorWithMetadata(error)
if (!e) {
return error
}
const gitError = asGitError(e.underlyingError)
if (!gitError) {
return error
}
const { repository } = e.metadata
if (!(repository instanceof Repository)) {
return error
}
if (repository.gitHubRepository === null) {
return error
}
// DotCom only for now.
if (repository.gitHubRepository.endpoint !== getDotComAPIEndpoint()) {
return error
}
const match = rejectedPathRe.exec(error.message)
if (!match) {
return error
}
const rejectedPath = match[1]
const pathIsLikelyWorkflowFile =
rejectedPath.startsWith('.github/') && rejectedPath.indexOf('workflow') >= 0
if (!pathIsLikelyWorkflowFile) {
return error
}
dispatcher.showPopup({
type: PopupType.PushRejectedDueToMissingWorkflowScope,
rejectedPath,
repository,
})
return null
}

View file

@ -21,6 +21,7 @@ import {
upstreamAlreadyExistsHandler,
rebaseConflictsHandler,
localChangesOverwrittenHandler,
refusedWorkflowUpdate,
} from './dispatcher'
import {
AppStore,
@ -261,6 +262,7 @@ dispatcher.registerErrorHandler(backgroundTaskHandler)
dispatcher.registerErrorHandler(missingRepositoryHandler)
dispatcher.registerErrorHandler(localChangesOverwrittenHandler)
dispatcher.registerErrorHandler(rebaseConflictsHandler)
dispatcher.registerErrorHandler(refusedWorkflowUpdate)
document.body.classList.add(`platform-${process.platform}`)

View file

@ -6,8 +6,18 @@ import { Form } from './form'
import { Button } from './button'
import { TextBox } from './text-box'
import { Errors } from './errors'
import { getDotComAPIEndpoint } from '../../lib/api'
interface IAuthenticationFormProps {
/**
* The URL to the host which we're currently authenticating
* against. This will be either https://api.github.com when
* signing in against GitHub.com or a user-specified
* URL when signing in against a GitHub Enterprise Server
* instance.
*/
readonly endpoint: string
/** Does the server support basic auth? */
readonly supportsBasicAuth: boolean
@ -149,12 +159,7 @@ export class AuthenticationForm extends React.Component<
return (
<div>
{basicAuth ? <hr className="short-rule" /> : null}
{basicAuth ? null : (
<p>
Your GitHub Enterprise Server instance requires you to sign in with
your browser.
</p>
)}
{basicAuth ? null : this.renderEndpointRequiresWebFlow()}
<div className="sign-in-footer">
{basicAuth ? browserSignInLink : browserSignInButton}
@ -164,6 +169,31 @@ export class AuthenticationForm extends React.Component<
)
}
private renderEndpointRequiresWebFlow() {
if (this.props.endpoint === getDotComAPIEndpoint()) {
return (
<>
<p>
To improve the security of your account, GitHub now requires you to
sign in through your browser.
</p>
<p>
Your browser will redirect you back to GitHub Desktop once you've
signed in. If your browser asks for your permission to launch GitHub
Desktop please allow it to.
</p>
</>
)
} else {
return (
<p>
Your GitHub Enterprise Server instance requires you to sign in with
your browser.
</p>
)
}
}
private renderError() {
const error = this.props.error
if (!error) {

View file

@ -25,6 +25,9 @@ interface IConfigureGitUserProps {
}
interface IConfigureGitUserState {
readonly globalUserName: string | null
readonly globalUserEmail: string | null
readonly name: string
readonly email: string
readonly avatarURL: string | null
@ -39,30 +42,86 @@ export class ConfigureGitUser extends React.Component<
IConfigureGitUserProps,
IConfigureGitUserState
> {
private readonly globalUsernamePromise = getGlobalConfigValue('user.name')
private readonly globalEmailPromise = getGlobalConfigValue('user.email')
public constructor(props: IConfigureGitUserProps) {
super(props)
this.state = { name: '', email: '', avatarURL: null }
this.state = {
globalUserName: null,
globalUserEmail: null,
name: '',
email: '',
avatarURL: null,
}
}
public async componentWillMount() {
let name = await getGlobalConfigValue('user.name')
let email = await getGlobalConfigValue('user.email')
public async componentDidMount() {
// Capture the current accounts prop because we'll be
// doing a bunch of asynchronous stuff and we can't
// rely on this.props.account to tell us what that prop
// was at mount-time.
const accounts = this.props.accounts
const user = this.props.accounts[0]
if ((!name || !name.length) && user) {
name = user.name && user.name.length ? user.name : user.login
}
const [globalUserName, globalUserEmail] = await Promise.all([
this.globalUsernamePromise,
this.globalEmailPromise,
])
if ((!email || !email.length) && user) {
const found = lookupPreferredEmail(user.emails)
if (found) {
email = found.email
this.setState(
prevState => ({
globalUserName,
globalUserEmail,
name:
prevState.name.length === 0 ? globalUserName || '' : prevState.name,
email:
prevState.email.length === 0
? globalUserEmail || ''
: prevState.email,
}),
() => {
// Chances are low that we actually have an account at mount-time
// the way things are designed now but in case the app changes around
// us and we do get passed an account at mount time in the future we
// want to make sure that not only was it passed at mount time but also
// that it hasn't been changed since (if it has been then
// componentDidUpdate would be responsible for handling it).
if (accounts === this.props.accounts && accounts.length > 0) {
this.setDefaultValuesFromAccount(accounts[0])
}
}
)
}
public componentDidUpdate(prevProps: IConfigureGitUserProps) {
if (
this.props.accounts !== prevProps.accounts &&
this.props.accounts.length > 0
) {
if (this.props.accounts[0] !== prevProps.accounts[0]) {
const account = this.props.accounts[0]
this.setDefaultValuesFromAccount(account)
}
}
}
const avatarURL = email ? this.avatarURLForEmail(email) : null
this.setState({ name: name || '', email: email || '', avatarURL })
private setDefaultValuesFromAccount(account: Account) {
if (this.state.name.length === 0) {
this.setState({
name: account.name || account.login,
})
}
if (this.state.email.length === 0) {
const preferredEmail = lookupPreferredEmail(account)
if (preferredEmail) {
this.setState({
email: preferredEmail.email,
avatarURL: this.avatarURLForEmail(preferredEmail.email),
})
}
}
}
private dateWithMinuteOffset(date: Date, minuteOffset: number): Date {
@ -156,18 +215,18 @@ export class ConfigureGitUser extends React.Component<
}
private save = async () => {
if (this.props.onSave) {
this.props.onSave()
}
const { name, email, globalUserName, globalUserEmail } = this.state
const name = this.state.name
if (name.length) {
if (name.length > 0 && name !== globalUserName) {
await setGlobalConfigValue('user.name', name)
}
const email = this.state.email
if (email.length) {
if (email.length > 0 && email !== globalUserEmail) {
await setGlobalConfigValue('user.email', email)
}
if (this.props.onSave) {
this.props.onSave()
}
}
}

View file

@ -122,7 +122,7 @@ const renderResolvedFile: React.SFC<{
<li key={props.path} className="unmerged-file-status-resolved">
<Octicon symbol={OcticonSymbol.fileCode} className="file-octicon" />
<div className="column-left">
<PathText path={props.path} availableWidth={200} />
<PathText path={props.path} />
{renderResolvedFileStatusSummary({
path: props.path,
status: props.status,
@ -160,7 +160,7 @@ const renderManualConflictedFile: React.SFC<{
const content = (
<>
<div className="column-left">
<PathText path={props.path} availableWidth={200} />
<PathText path={props.path} />
<div className="file-conflicts-status">{manualConflictString}</div>
</div>
<div className="action-buttons">
@ -217,7 +217,7 @@ const renderConflictedFileWithConflictMarkers: React.SFC<{
const content = (
<>
<div className="column-left">
<PathText path={props.path} availableWidth={200} />
<PathText path={props.path} />
<div className="file-conflicts-status">{message}</div>
</div>
<div className="action-buttons">

View file

@ -258,12 +258,30 @@ export class PathText extends React.PureComponent<
public componentDidMount() {
this.resizeIfNecessary()
document.addEventListener('dialog-show', this.onDialogShow)
}
public componentWillUnmount() {
document.removeEventListener('dialog-show', this.onDialogShow)
}
public componentDidUpdate() {
this.resizeIfNecessary()
}
// In case this component is contained within a <dialog>, make sure to resize
// it after the dialog element is shown in order to apply correct layout.
// https://github.com/desktop/desktop/issues/6666
private onDialogShow = (event: Event) => {
const dialogElement = event.target
if (
dialogElement instanceof Element &&
dialogElement.contains(this.pathElement)
) {
this.resizeIfNecessary()
}
}
private onPathElementRef = (element: HTMLDivElement | null) => {
this.pathElement = element
}

View file

@ -63,6 +63,7 @@ export class SignIn extends React.Component<ISignInProps, {}> {
onBrowserSignInRequested={this.onBrowserSignInRequested}
onSubmit={this.onCredentialsEntered}
forgotPasswordUrl={state.forgotPasswordUrl}
endpoint={state.endpoint}
/>
)
}

View file

@ -50,10 +50,6 @@ export class MergeConflictsDialog extends React.Component<
IMergeConflictsDialogProps,
{}
> {
public async componentDidMount() {
this.props.dispatcher.resolveCurrentEditor()
}
/**
* commits the merge displays the repository changes tab and dismisses the modal
*/

View file

@ -246,6 +246,13 @@ export class OcticonSymbol {
'M6 15c-3.31 0-6-.9-6-2v-2c0-.17.09-.34.21-.5.67.86 3 1.5 5.79 1.5s5.12-.64 5.79-1.5c.13.16.21.33.21.5v2c0 1.1-2.69 2-6 2zm0-4c-3.31 0-6-.9-6-2V7c0-.11.04-.21.09-.31.03-.06.07-.13.12-.19C.88 7.36 3.21 8 6 8s5.12-.64 5.79-1.5c.05.06.09.13.12.19.05.1.09.21.09.31v2c0 1.1-2.69 2-6 2zm0-4c-3.31 0-6-.9-6-2V3c0-1.1 2.69-2 6-2s6 .9 6 2v2c0 1.1-2.69 2-6 2zm0-5c-2.21 0-4 .45-4 1s1.79 1 4 1 4-.45 4-1-1.79-1-4-1z'
)
}
public static get dependent() {
return new OcticonSymbol(
16,
16,
'M1 1h7.5l2 2H9L8 2H1v12h10v-1h1v1c0 .55-.45 1-1 1H1c-.55 0-1-.45-1-1V2c0-.55.45-1 1-1zm9 6h3v1h-3V7zm2 2h-2v1h2V9zM8.583 4h4.375L15 6v5.429a.58.58 0 0 1-.583.571H8.583A.58.58 0 0 1 8 11.429V10h1v1h5V6.5L12.5 5H9v1H8V4.571A.58.58 0 0 1 8.583 4zM9.5 7H6.667V5l-4 3 4 3V9H9.5V7z'
)
}
public static get desktopDownload() {
return new OcticonSymbol(
16,
@ -958,6 +965,20 @@ export class OcticonSymbol {
'M4 3H3V2h1v1zM3 5h1V4H3v1zm4 0L4 9h2v7h2V9h2L7 5zm4-5H1C.45 0 0 .45 0 1v12c0 .55.45 1 1 1h4v-1H1v-2h4v-1H2V1h9.02L11 10H9v1h2v2H9v1h2c.55 0 1-.45 1-1V1c0-.55-.45-1-1-1z'
)
}
public static get repoTemplate() {
return new OcticonSymbol(
14,
16,
'M12 8V1c0-.55-.45-1-1-1H1C.45 0 0 .45 0 1v12c0 .55.45 1 1 1h2v2l1.5-1.5L6 16v-4H3v1H1v-2h7v-1H2V1h9v7h1zM4 2H3v1h1V2zM3 4h1v1H3V4zm1 2H3v1h1V6zm0 3H3V8h1v1zm6 3H8v2h2v2h2v-2h2v-2h-2v-2h-2v2z'
)
}
public static get repoTemplatePrivate() {
return new OcticonSymbol(
14,
16,
'M12 6c0-.55-.45-1-1-1h-1V4c0-2.2-1.8-4-4-4S2 1.8 2 4v1H1c-.55 0-1 .45-1 1v7c0 .55.45 1 1 1h5v-1H2V6h9v2h1V6zM8.21 5V4c0-1.22-.98-2.2-2.2-2.2-1.22 0-2.2.98-2.2 2.2v1h4.4zM12 12h2v2h-2v2h-2v-2H8v-2h2v-2h2v2zm-9 0h1v-1H3v1zm0-5h1v1H3V7zm1 2H3v1h1V9z'
)
}
public static get report() {
return new OcticonSymbol(
16,
@ -965,6 +986,13 @@ export class OcticonSymbol {
'M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H7l-4 4v-4H1a1 1 0 0 1-1-1V2zm1 0h14v9H6.5L4 13.5V11H1V2zm6 6h2v2H7V8zm0-5h2v4H7V3z'
)
}
public static get requestChanges() {
return new OcticonSymbol(
16,
15,
'M0 1a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H7.5L4 15.5V12H1a1 1 0 0 1-1-1V1zm1 0v10h4v2l2-2h8V1H1zm7.5 3h2v1h-2v2h-1V5h-2V4h2V2h1v2zm2 5h-5V8h5v1z'
)
}
public static get rocket() {
return new OcticonSymbol(
16,
@ -1022,12 +1050,33 @@ export class OcticonSymbol {
)
}
public static get shield() {
return new OcticonSymbol(
14,
16,
'M0 2l7-2 7 2v6.02C14 12.69 8.69 16 7 16c-1.69 0-7-3.31-7-7.98V2zm1 .75L7 1l6 1.75v5.268C13 12.104 8.449 15 7 15c-1.449 0-6-2.896-6-6.982V2.75zm1 .75L7 2v12c-1.207 0-5-2.482-5-5.985V3.5z'
)
}
public static get shieldCheck() {
return new OcticonSymbol(
16,
16,
'M6.5 0L0 1.875v5.644C0 11.897 4.93 15 6.5 15c.741 0 2.232-.692 3.6-1.884l-.713-.61C8.275 13.453 7.099 14 6.5 14 5.172 14 1 11.31 1 7.516V2.625L6.5 1 12 2.625v4.891c0 .128-.005.255-.014.38L13 6.713V1.875L6.5 0zm5 10l-2-1.5L8 10l3.5 3L16 8l-1.5-1.5-3 3.5zM2 3.375L6.5 2v11C5.414 13 2 10.724 2 7.514V3.375z'
)
}
public static get shieldLock() {
return new OcticonSymbol(
14,
16,
'M7 0L0 2v6.02C0 12.69 5.31 16 7 16c1.69 0 7-3.31 7-7.98V2L7 0zM5 11l1.14-2.8a.568.568 0 0 0-.25-.59C5.33 7.25 5 6.66 5 6c0-1.09.89-2 1.98-2C8.06 4 9 4.91 9 6c0 .66-.33 1.25-.89 1.61-.19.13-.3.36-.25.59L9 11H5z'
)
}
public static get shieldX() {
return new OcticonSymbol(
16,
16,
'M6.5 0L0 1.875v5.644C0 11.897 4.93 15 6.5 15c.63 0 1.8-.5 2.976-1.38l-.663-.663C7.889 13.625 6.996 14 6.5 14 5.172 14 1 11.31 1 7.516V2.625L6.5 1 12 2.625v4.23l.48.48.52-.52v-4.94L6.5 0zm5.98 8.75L10.73 7 9.25 8.48 11 10.23l-1.75 1.75 1.48 1.48 1.75-1.75 1.75 1.75 1.48-1.48-1.75-1.75 1.75-1.75L14.23 7l-1.75 1.75zM2 3.375L6.5 2v11C5.414 13 2 10.724 2 7.514V3.375z'
)
}
public static get signIn() {
return new OcticonSymbol(
14,
@ -1042,6 +1091,13 @@ export class OcticonSymbol {
'M12 9V7H8V5h4V3l4 3-4 3zm-2 3H6V3L2 1h8v3h1V1c0-.55-.45-1-1-1H1C.45 0 0 .45 0 1v11.38c0 .39.22.73.55.91L6 16.01V13h4c.55 0 1-.45 1-1V8h-1v4z'
)
}
public static get skip() {
return new OcticonSymbol(
16,
16,
'M5.79 11.624l-1.326-.088-.088-1.326 5.834-5.834 1.326.088.088 1.326-5.834 5.834zM8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14zm5.5-7a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z'
)
}
public static get smiley() {
return new OcticonSymbol(
16,
@ -1123,14 +1179,14 @@ export class OcticonSymbol {
return new OcticonSymbol(
16,
16,
'M16.968 8.314l-1.03-6.318C15.758.531 13.942 0 12.742 0h-6.7a1.08 1.08 0 0 0-.563.149l-1.529.913H2.124C.998 1.062 0 2.06 0 3.186v4.247c0 1.125.998 2.145 2.124 2.124h2.123c.967 0 1.476.477 2.538 1.645.966 1.062.935 1.912.67 3.473-.086.53.063 1.061.445 1.507.414.5 1.04.818 1.657.818 1.943 0 3.185-3.95 3.185-5.33l-.021-1.041h2.166c1.232 0 2.07-.85 2.102-2.092 0-.064.022-.138-.02-.212v-.01zm-2.092 1.264h-2.113c-.743 0-1.093.297-1.093 1.03l.03 1.092c0 1.349-1.242 4.248-2.123 4.248-.531 0-1.147-.531-1.062-1.062.265-1.678.361-2.952-.945-4.396-1.083-1.2-1.88-1.996-3.324-1.996v-6.37L6.02 1.062h6.721c.775 0 2.07.329 2.124 1.062l.02.02 1.063 6.372c-.032.68-.404 1.062-1.062 1.062h-.01z'
'M15.98 7.83l-.97-5.95C14.84.5 13.13 0 12 0H5.69c-.2 0-.38.05-.53.14L3.72 1H2C.94 1 0 1.94 0 3v4c0 1.06.94 2.02 2 2h2c.91 0 1.39.45 2.39 1.55.91 1 .88 1.8.63 3.27-.08.5.06 1 .42 1.42.39.47.98.76 1.56.76 1.83 0 3-3.71 3-5.01l-.02-.98h2.04c1.16 0 1.95-.8 1.98-1.97 0-.11-.02-.21-.02-.21zm-1.97 1.19h-1.99c-.7 0-1.03.28-1.03.97l.03 1.03c0 1.27-1.17 4-2 4-.5 0-1.08-.5-1-1 .25-1.58.34-2.78-.89-4.14C6.11 8.75 5.36 8 4 8V2l1.67-1H12c.73 0 1.95.31 2 1l.02.02 1 6c-.03.64-.38 1-1 1h-.01z'
)
}
public static get thumbsup() {
return new OcticonSymbol(
16,
16,
'M14 14c-.05.69-1.27 1-2 1H5.67L4 14V8c1.36 0 2.11-.75 3.13-1.88 1.23-1.36 1.14-2.56.88-4.13-.08-.5.5-1 1-1 .83 0 2 2.73 2 4l-.02 1.03c0 .69.33.97 1.02.97h2c.63 0 .98.36 1 1l-1 6L14 14zm0-8h-2.02l.02-.98C12 3.72 10.83 0 9 0c-.58 0-1.17.3-1.56.77-.36.41-.5.91-.42 1.41.25 1.48.28 2.28-.63 3.28-1 1.09-1.48 1.55-2.39 1.55H2C.94 7 0 7.94 0 9v4c0 1.06.94 2 2 2h1.72l1.44.86c.16.09.33.14.52.14h6.33c1.13 0 2.84-.5 3-1.88l.98-5.95c.02-.08.02-.14.02-.2-.03-1.17-.84-1.97-2-1.97H14z'
'M15.98 8.17l-.97 5.95C14.84 15.5 13.13 16 12 16H5.69c-.2 0-.38-.05-.53-.14L3.72 15H2c-1.06 0-2-.94-2-2V9c0-1.06.94-2.02 2-2h2c.91 0 1.39-.45 2.39-1.55.91-1 .88-1.8.63-3.27-.08-.5.06-1 .42-1.42C7.83.29 8.42 0 9 0c1.83 0 3 3.71 3 5.01l-.02.98h2.04c1.16 0 1.95.8 1.98 1.97 0 .11-.02.21-.02.21zm-1.97-1.19h-1.99c-.7 0-1.03-.28-1.03-.97l.03-1.03c0-1.27-1.17-4-2-4-.5 0-1.08.5-1 1 .25 1.58.34 2.78-.89 4.14C6.11 7.25 5.36 8 4 8v6l1.67 1H12c.73 0 1.95-.31 2-1l.02-.02 1-6c-.03-.64-.38-1-1-1h-.01z'
)
}
public static get tools() {
@ -1177,14 +1233,14 @@ export class OcticonSymbol {
return new OcticonSymbol(
16,
16,
'M16.65 7.507l-1.147-1.423a1.595 1.595 0 0 1-.33-.817l-.201-1.805a1.603 1.603 0 0 0-1.412-1.413l-1.806-.201a1.617 1.617 0 0 1-.828-.35L9.503.35a1.597 1.597 0 0 0-1.996 0L6.084 1.497c-.233.18-.51.297-.817.33l-1.805.201A1.603 1.603 0 0 0 2.049 3.44l-.201 1.805c-.032.319-.17.595-.35.829L.35 7.497a1.597 1.597 0 0 0 0 1.996l1.147 1.423c.18.233.297.51.33.817l.201 1.805a1.603 1.603 0 0 0 1.412 1.413l1.805.201c.319.032.595.17.829.35l1.423 1.148a1.597 1.597 0 0 0 1.996 0l1.423-1.147c.233-.18.51-.297.817-.33l1.805-.201a1.603 1.603 0 0 0 1.413-1.412l.201-1.806c.032-.318.17-.594.35-.828l1.148-1.423a1.597 1.597 0 0 0 0-1.996zm-7.083 4.715c0 .297-.233.53-.53.53H7.973a.532.532 0 0 1-.53-.53V11.16c0-.297.244-.531.53-.531h1.062c.298 0 .531.234.531.53v1.063zm1.657-5.193c-.064.18-.18.35-.319.5-.138.17-.149.201-.35.403a3.5 3.5 0 0 1-.553.478c-.116.095-.212.201-.297.286-.085.085-.148.18-.202.287a1.63 1.63 0 0 0-.116.319c-.032.116-.032.138-.032.265H7.582c0-.233 0-.329.031-.51.032-.201.085-.382.149-.552.064-.148.149-.297.265-.446.117-.138.245-.265.436-.403.287-.202.382-.319.51-.552.127-.234.212-.404.212-.627 0-.287-.064-.478-.212-.616-.139-.138-.33-.201-.616-.201-.096 0-.202.02-.319.053-.117.032-.18.095-.265.17-.085.074-.149.116-.213.212a.435.435 0 0 0-.095.297H5.34c0-.403.138-.594.287-.881.17-.287.382-.531.647-.712.266-.18.584-.318.935-.403a4.94 4.94 0 0 1 1.157-.138c.467 0 .882.053 1.243.138.36.096.669.234.934.414.244.18.435.404.584.67.138.265.202.583.202.933 0 .234 0 .446-.085.627l-.021-.01z'
'M15.67 7.066l-1.08-1.34a1.5 1.5 0 0 1-.309-.77l-.19-1.698a1.51 1.51 0 0 0-1.329-1.33l-1.699-.19c-.3-.03-.56-.159-.78-.329L8.945.33a1.504 1.504 0 0 0-1.878 0l-1.34 1.08a1.5 1.5 0 0 1-.77.31l-1.698.19c-.7.08-1.25.63-1.33 1.329l-.19 1.699c-.03.3-.159.56-.329.78L.33 7.055a1.504 1.504 0 0 0 0 1.878l1.08 1.34c.17.22.28.48.31.77l.19 1.698c.08.7.63 1.25 1.329 1.33l1.699.19c.3.03.56.159.78.329l1.339 1.08c.55.439 1.329.439 1.878 0l1.34-1.08c.22-.17.48-.28.77-.31l1.698-.19c.7-.08 1.25-.63 1.33-1.329l.19-1.699c.03-.3.159-.56.329-.78l1.08-1.339a1.504 1.504 0 0 0 0-1.878zM9 11.5c0 .28-.22.5-.5.5h-1c-.27 0-.5-.22-.5-.5v-1c0-.28.23-.5.5-.5h1c.28 0 .5.22.5.5v1zm1.56-4.89c-.06.17-.17.33-.3.47-.13.16-.14.19-.33.38-.16.17-.31.3-.52.45-.11.09-.2.19-.28.27-.08.08-.14.17-.19.27-.05.1-.08.19-.11.3-.03.11-.03.13-.03.25H7.13c0-.22 0-.31.03-.48.03-.19.08-.36.14-.52.06-.14.14-.28.25-.42.11-.13.23-.25.41-.38.27-.19.36-.3.48-.52.12-.22.2-.38.2-.59 0-.27-.06-.45-.2-.58-.13-.13-.31-.19-.58-.19-.09 0-.19.02-.3.05-.11.03-.17.09-.25.16-.08.07-.14.11-.2.2a.41.41 0 0 0-.09.28h-2c0-.38.13-.56.27-.83.16-.27.36-.5.61-.67.25-.17.55-.3.88-.38.33-.08.7-.13 1.09-.13.44 0 .83.05 1.17.13.34.09.63.22.88.39.23.17.41.38.55.63.13.25.19.55.19.88 0 .22 0 .42-.08.59l-.02-.01z'
)
}
public static get verified() {
return new OcticonSymbol(
16,
16,
'M16.65 7.507l-1.147-1.423a1.595 1.595 0 0 1-.33-.817l-.201-1.805a1.603 1.603 0 0 0-1.412-1.413l-1.806-.201a1.617 1.617 0 0 1-.828-.35L9.503.35a1.597 1.597 0 0 0-1.996 0L6.084 1.497c-.233.18-.51.297-.817.33l-1.805.201A1.603 1.603 0 0 0 2.049 3.44l-.201 1.805c-.032.319-.17.595-.35.829L.35 7.497a1.597 1.597 0 0 0 0 1.996l1.147 1.423c.18.233.297.51.33.817l.201 1.805a1.603 1.603 0 0 0 1.412 1.413l1.805.201c.319.032.595.17.829.35l1.423 1.148a1.597 1.597 0 0 0 1.996 0l1.423-1.147c.233-.18.51-.297.817-.33l1.805-.201a1.603 1.603 0 0 0 1.413-1.412l.201-1.806c.032-.318.17-.594.35-.828l1.148-1.423a1.597 1.597 0 0 0 0-1.996zm-9.737 5.246L3.196 9.036 4.79 7.443l2.124 2.124 5.309-5.309 1.593 1.646-6.902 6.849z'
'M15.67 7.066l-1.08-1.34a1.5 1.5 0 0 1-.309-.77l-.19-1.698a1.51 1.51 0 0 0-1.329-1.33l-1.699-.19c-.3-.03-.56-.159-.78-.329L8.945.33a1.504 1.504 0 0 0-1.878 0l-1.34 1.08a1.5 1.5 0 0 1-.77.31l-1.698.19c-.7.08-1.25.63-1.33 1.329l-.19 1.699c-.03.3-.159.56-.329.78L.33 7.055a1.504 1.504 0 0 0 0 1.878l1.08 1.34c.17.22.28.48.31.77l.19 1.698c.08.7.63 1.25 1.329 1.33l1.699.19c.3.03.56.159.78.329l1.339 1.08c.55.439 1.329.439 1.878 0l1.34-1.08c.22-.17.48-.28.77-.31l1.698-.19c.7-.08 1.25-.63 1.33-1.329l.19-1.699c.03-.3.159-.56.329-.78l1.08-1.339a1.504 1.504 0 0 0 0-1.878zM6.5 12.01L3 8.51l1.5-1.5 2 2 5-5L13 5.56l-6.5 6.45z'
)
}
public static get versions() {

View file

@ -41,7 +41,7 @@ export class Accounts extends React.Component<IAccountsProps, {}> {
}
private renderAccount(account: Account) {
const found = lookupPreferredEmail(account.emails)
const found = lookupPreferredEmail(account)
const email = found ? found.email : ''
const avatarUser: IAvatarUser = {

View file

@ -96,7 +96,7 @@ export class Preferences extends React.Component<
}
if (!committerEmail) {
const found = lookupPreferredEmail(account.emails)
const found = lookupPreferredEmail(account)
if (found) {
committerEmail = found.email
}

View file

@ -61,10 +61,6 @@ export class ShowConflictedFilesDialog extends React.Component<
}
}
public componentDidMount() {
this.props.dispatcher.resolveCurrentEditor()
}
public componentWillUnmount() {
const { workingDirectory, step, userHasResolvedConflicts } = this.props
const { conflictState } = step

View file

@ -1,5 +1,5 @@
import * as React from 'react'
import { Repository as Repo } from '../models/repository'
import { Repository } from '../models/repository'
import { Commit } from '../models/commit'
import { TipState } from '../models/tip'
import { UiView } from './ui-view'
@ -25,12 +25,16 @@ import { ImageDiffType } from '../models/diff'
import { IMenu } from '../models/app-menu'
import { StashDiffViewer } from './stashing'
import { StashedChangesLoadStates } from '../models/stash-entry'
import { TutorialPanel, TutorialWelcome, TutorialDone } from './tutorial'
import { enableTutorial } from '../lib/feature-flag'
import { TutorialStep, isValidTutorialStep } from '../models/tutorial-step'
import { ExternalEditor } from '../lib/editors'
/** The widest the sidebar can be with the minimum window size. */
const MaxSidebarWidth = 495
interface IRepositoryViewProps {
readonly repository: Repo
readonly repository: Repository
readonly state: IRepositoryState
readonly dispatcher: Dispatcher
readonly emoji: Map<string, string>
@ -49,6 +53,9 @@ interface IRepositoryViewProps {
/** The name of the currently selected external editor */
readonly externalEditorLabel?: string
/** A cached entry representing an external editor found on the user's machine */
readonly resolvedExternalEditor: ExternalEditor | null
/**
* Callback to open a selected file using the configured external editor
*
@ -60,6 +67,10 @@ interface IRepositoryViewProps {
* The top-level application menu item.
*/
readonly appMenu: IMenu | undefined
readonly currentTutorialStep: TutorialStep
readonly onExitTutorial: () => void
}
interface IRepositoryViewState {
@ -276,23 +287,69 @@ export class RepositoryView extends React.Component<
return null
}
private renderContent(): JSX.Element | null {
const selectedSection = this.props.state.selectedSection
if (selectedSection === RepositorySectionTab.Changes) {
const { changesState } = this.props.state
const { workingDirectory, selection } = changesState
private renderContentForHistory(): JSX.Element {
const { commitSelection } = this.props.state
if (selection.kind === ChangesSelectionKind.Stash) {
return this.renderStashedChangesContent()
}
const sha = commitSelection.sha
const { selectedFileIDs, diff } = selection
const selectedCommit =
sha != null ? this.props.state.commitLookup.get(sha) || null : null
if (selectedFileIDs.length > 1) {
return <MultipleSelection count={selectedFileIDs.length} />
}
const { changedFiles, file, diff } = commitSelection
if (workingDirectory.files.length === 0) {
return (
<SelectedCommit
repository={this.props.repository}
dispatcher={this.props.dispatcher}
selectedCommit={selectedCommit}
changedFiles={changedFiles}
selectedFile={file}
currentDiff={diff}
emoji={this.props.emoji}
commitSummaryWidth={this.props.commitSummaryWidth}
gitHubUsers={this.props.state.gitHubUsers}
selectedDiffType={this.props.imageDiffType}
externalEditorLabel={this.props.externalEditorLabel}
onOpenInExternalEditor={this.props.onOpenInExternalEditor}
hideWhitespaceInDiff={this.props.hideWhitespaceInDiff}
/>
)
}
private renderTutorialPane(): JSX.Element {
if (this.props.currentTutorialStep === TutorialStep.AllDone) {
return (
<TutorialDone
dispatcher={this.props.dispatcher}
repository={this.props.repository}
/>
)
} else {
return <TutorialWelcome />
}
}
private renderContentForChanges(): JSX.Element | null {
const { changesState } = this.props.state
const { workingDirectory, selection } = changesState
if (selection.kind === ChangesSelectionKind.Stash) {
return this.renderStashedChangesContent()
}
const { selectedFileIDs, diff } = selection
if (selectedFileIDs.length > 1) {
return <MultipleSelection count={selectedFileIDs.length} />
}
if (workingDirectory.files.length === 0) {
if (
enableTutorial() &&
this.props.currentTutorialStep !== TutorialStep.NotApplicable
) {
return this.renderTutorialPane()
} else {
return (
<NoChanges
key={this.props.repository.id}
@ -305,56 +362,38 @@ export class RepositoryView extends React.Component<
dispatcher={this.props.dispatcher}
/>
)
} else {
if (selectedFileIDs.length === 0 || diff === null) {
return null
}
const selectedFile = workingDirectory.findFileWithID(selectedFileIDs[0])
if (selectedFile === null) {
return null
}
return (
<Changes
repository={this.props.repository}
dispatcher={this.props.dispatcher}
file={selectedFile}
diff={diff}
isCommitting={this.props.state.isCommitting}
imageDiffType={this.props.imageDiffType}
hideWhitespaceInDiff={this.props.hideWhitespaceInDiff}
/>
)
}
} else if (selectedSection === RepositorySectionTab.History) {
const { commitSelection } = this.props.state
} else {
if (selectedFileIDs.length === 0 || diff === null) {
return null
}
const sha = commitSelection.sha
const selectedFile = workingDirectory.findFileWithID(selectedFileIDs[0])
const selectedCommit =
sha != null ? this.props.state.commitLookup.get(sha) || null : null
const { changedFiles, file, diff } = commitSelection
if (selectedFile === null) {
return null
}
return (
<SelectedCommit
<Changes
repository={this.props.repository}
dispatcher={this.props.dispatcher}
selectedCommit={selectedCommit}
changedFiles={changedFiles}
selectedFile={file}
currentDiff={diff}
emoji={this.props.emoji}
commitSummaryWidth={this.props.commitSummaryWidth}
gitHubUsers={this.props.state.gitHubUsers}
selectedDiffType={this.props.imageDiffType}
externalEditorLabel={this.props.externalEditorLabel}
onOpenInExternalEditor={this.props.onOpenInExternalEditor}
file={selectedFile}
diff={diff}
isCommitting={this.props.state.isCommitting}
imageDiffType={this.props.imageDiffType}
hideWhitespaceInDiff={this.props.hideWhitespaceInDiff}
/>
)
}
}
private renderContent(): JSX.Element | null {
const selectedSection = this.props.state.selectedSection
if (selectedSection === RepositorySectionTab.Changes) {
return this.renderContentForChanges()
} else if (selectedSection === RepositorySectionTab.History) {
return this.renderContentForHistory()
} else {
return assertNever(selectedSection, 'Unknown repository section')
}
@ -365,6 +404,7 @@ export class RepositoryView extends React.Component<
<UiView id="repository" onKeyDown={this.onKeyDown}>
{this.renderSidebar()}
{this.renderContent()}
{this.maybeRenderTutorialPanel()}
</UiView>
)
}
@ -407,4 +447,22 @@ export class RepositoryView extends React.Component<
})
}
}
private maybeRenderTutorialPanel(): JSX.Element | null {
if (
enableTutorial() &&
isValidTutorialStep(this.props.currentTutorialStep)
) {
return (
<TutorialPanel
dispatcher={this.props.dispatcher}
repository={this.props.repository}
resolvedExternalEditor={this.props.resolvedExternalEditor}
currentTutorialStep={this.props.currentTutorialStep}
onExitTutorial={this.props.onExitTutorial}
/>
)
}
return null
}
}

View file

@ -17,6 +17,7 @@ import { ButtonGroup } from '../lib/button-group'
import { Dialog, DialogError, DialogContent, DialogFooter } from '../dialog'
import { getWelcomeMessage } from '../../lib/2fa'
import { getDotComAPIEndpoint } from '../../lib/api'
interface ISignInProps {
readonly dispatcher: Dispatcher
@ -31,6 +32,12 @@ interface ISignInState {
readonly otpToken: string
}
const SignInWithBrowserTitle = __DARWIN__
? 'Sign in Using Your Browser'
: 'Sign in using your browser'
const DefaultTitle = 'Sign in'
export class SignIn extends React.Component<ISignInProps, ISignInState> {
public constructor(props: ISignInProps) {
super(props)
@ -176,14 +183,30 @@ export class SignIn extends React.Component<ISignInProps, ISignInState> {
private renderAuthenticationStep(state: IAuthenticationState) {
if (!state.supportsBasicAuth) {
return (
<DialogContent>
<p>
Your GitHub Enterprise Server instance requires you to sign in with
your browser.
</p>
</DialogContent>
)
if (state.endpoint === getDotComAPIEndpoint()) {
return (
<DialogContent>
<p>
To improve the security of your account, GitHub now requires you
to sign in through your browser.
</p>
<p>
Your browser will redirect you back to GitHub Desktop once you've
signed in. If your browser asks for your permission to launch
GitHub Desktop please allow it to.
</p>
</DialogContent>
)
} else {
return (
<DialogContent>
<p>
Your GitHub Enterprise Server instance requires you to sign in
with your browser.
</p>
</DialogContent>
)
}
}
const disableSubmit = state.loading
@ -288,10 +311,17 @@ export class SignIn extends React.Component<ISignInProps, ISignInState> {
<DialogError>{state.error.message}</DialogError>
) : null
const title =
this.props.signInState &&
this.props.signInState.kind === SignInStep.Authentication &&
!this.props.signInState.supportsBasicAuth
? SignInWithBrowserTitle
: DefaultTitle
return (
<Dialog
id="sign-in"
title="Sign in"
title={title}
disabled={disabled}
onDismissed={this.props.onDismissed}
onSubmit={this.onSubmit}

View file

@ -0,0 +1,40 @@
import * as React from 'react'
import { Button } from '../lib/button'
import { ButtonGroup } from '../lib/button-group'
import { DialogFooter, DialogContent, Dialog } from '../dialog'
interface IConfirmExitTutorialProps {
readonly onDismissed: () => void
readonly onContinue: () => void
}
export class ConfirmExitTutorial extends React.Component<
IConfirmExitTutorialProps,
{}
> {
public render() {
return (
<Dialog
title={__DARWIN__ ? 'Exit Tutorial' : 'Exit tutorial'}
onDismissed={this.props.onDismissed}
onSubmit={this.props.onContinue}
type="normal"
>
<DialogContent>
<p>
Are you sure you want to leave the tutorial? This will bring you
back to the home screen.
</p>
</DialogContent>
<DialogFooter>
<ButtonGroup>
<Button type="submit">Exit tutorial</Button>
<Button onClick={this.props.onDismissed}>Cancel</Button>
</ButtonGroup>
</DialogFooter>
</Dialog>
)
}
}

View file

@ -0,0 +1,124 @@
import * as React from 'react'
import { encodePathAsUrl } from '../../lib/path'
import { Button } from '../lib/button'
import { Dispatcher } from '../dispatcher'
import { Repository } from '../../models/repository'
import { PopupType } from '../../models/popup'
import { Octicon, OcticonSymbol } from '../octicons'
const ClappingHandsImage = encodePathAsUrl(
__dirname,
'static/admin-mentoring.svg'
)
interface ITutorialDoneProps {
readonly dispatcher: Dispatcher
/**
* The currently selected repository
*/
readonly repository: Repository
}
export class TutorialDone extends React.Component<ITutorialDoneProps, {}> {
public render() {
return (
<div id="no-changes">
<div className="content">
<div className="header">
<div className="text">
<h1>You're done!</h1>
<p>
Youve learned the basics on how to use GitHub Desktop. Here are
some suggestions for what to do next.
</p>
</div>
<img src={ClappingHandsImage} className="blankslate-image" />
</div>
{this.renderActions()}
</div>
</div>
)
}
private renderActions() {
return (
<ul className="actions">
{this.renderExploreProjects()}
{this.renderStartNewProject()}
{this.renderAddLocalRepo()}
</ul>
)
}
private renderExploreProjects() {
return (
<li className="blankslate-action">
<div className="image-wrapper">
<Octicon symbol={OcticonSymbol.telescope} />
</div>
<div className="text-wrapper">
<h2>Explore projects on GitHub</h2>
<p className="description">
Contribute to a project that interests you
</p>
</div>
<Button onClick={this.openDotcomExplore}>
{__DARWIN__ ? 'Open in Browser' : 'Open in browser'}
</Button>
</li>
)
}
private renderStartNewProject() {
return (
<li className="blankslate-action">
<div className="image-wrapper">
<Octicon symbol={OcticonSymbol.plus} />
</div>
<div className="text-wrapper">
<h2>Create a new repository</h2>
<p className="description">Get started on a brand new project</p>
</div>
<Button onClick={this.onCreateNewRepository}>
{__DARWIN__ ? 'Create Repository' : 'Create repository'}
</Button>
</li>
)
}
private renderAddLocalRepo() {
return (
<li className="blankslate-action">
<div className="image-wrapper">
<Octicon symbol={OcticonSymbol.fileDirectory} />
</div>
<div className="text-wrapper">
<h2>Add a local repository</h2>
<p className="description">
Work on an existing project in GitHub Desktop
</p>
</div>
<Button onClick={this.onAddExistingRepository}>
{__DARWIN__ ? 'Add Repository' : 'Add repository'}
</Button>
</li>
)
}
private openDotcomExplore = () => {
this.props.dispatcher.showGitHubExplore(this.props.repository)
}
private onCreateNewRepository = () => {
this.props.dispatcher.showPopup({
type: PopupType.CreateRepository,
})
}
private onAddExistingRepository = () => {
this.props.dispatcher.showPopup({
type: PopupType.AddRepository,
})
}
}

View file

@ -0,0 +1,4 @@
export { TutorialPanel } from './tutorial-panel'
export { TutorialWelcome } from './welcome'
export { TutorialDone } from './done'
export { ConfirmExitTutorial } from './confirm-exit-tutorial'

View file

@ -0,0 +1,401 @@
import * as React from 'react'
import { join } from 'path'
import { LinkButton } from '../lib/link-button'
import { Button } from '../lib/button'
import { Monospaced } from '../lib/monospaced'
import { Repository } from '../../models/repository'
import { Dispatcher } from '../dispatcher'
import { Octicon, OcticonSymbol } from '../octicons'
import {
ValidTutorialStep,
TutorialStep,
orderedTutorialSteps,
} from '../../models/tutorial-step'
import { encodePathAsUrl } from '../../lib/path'
import { ExternalEditor } from '../../lib/editors'
import { PopupType } from '../../models/popup'
import { PreferencesTab } from '../../models/preferences'
const TutorialPanelImage = encodePathAsUrl(
__dirname,
'static/required-status-check.svg'
)
interface ITutorialPanelProps {
readonly dispatcher: Dispatcher
readonly repository: Repository
/** name of the configured external editor
* (`undefined` if none is configured.)
*/
readonly resolvedExternalEditor: ExternalEditor | null
readonly currentTutorialStep: ValidTutorialStep
readonly onExitTutorial: () => void
}
interface ITutorialPanelState {
/** ID of the currently expanded tutorial step */
readonly currentlyOpenSectionId: ValidTutorialStep
}
/** The Onboarding Tutorial Panel
* Renders a list of expandable tutorial steps (`TutorialListItem`).
* Enforces only having one step expanded at a time through
* event callbacks and local state.
*/
export class TutorialPanel extends React.Component<
ITutorialPanelProps,
ITutorialPanelState
> {
public constructor(props: ITutorialPanelProps) {
super(props)
this.state = { currentlyOpenSectionId: this.props.currentTutorialStep }
}
private openTutorialFileInEditor = () => {
this.props.dispatcher.openInExternalEditor(
// TODO: tie this filename to a shared constant
// for tutorial repos
join(this.props.repository.path, 'README.md')
)
}
private openPullRequest = () => {
this.props.dispatcher.createPullRequest(this.props.repository)
}
private skipEditorInstall = () => {
this.props.dispatcher.skipPickEditorTutorialStep(this.props.repository)
}
private skipCreatePR = () => {
this.props.dispatcher.markPullRequestTutorialStepAsComplete(
this.props.repository
)
}
private isStepComplete = (step: ValidTutorialStep) => {
return (
orderedTutorialSteps.indexOf(step) <
orderedTutorialSteps.indexOf(this.props.currentTutorialStep)
)
}
private isStepNextTodo = (step: ValidTutorialStep) => {
return step === this.props.currentTutorialStep
}
public componentWillReceiveProps(nextProps: ITutorialPanelProps) {
if (this.props.currentTutorialStep !== nextProps.currentTutorialStep) {
this.setState({
currentlyOpenSectionId: nextProps.currentTutorialStep,
})
}
}
public render() {
return (
<div className="tutorial-panel-component panel">
<div className="titleArea">
<h3>Get started</h3>
<img src={TutorialPanelImage} />
</div>
<ol>
<TutorialStepInstructions
summaryText="Install a text editor"
isComplete={this.isStepComplete}
isNextStepTodo={this.isStepNextTodo}
sectionId={TutorialStep.PickEditor}
currentlyOpenSectionId={this.state.currentlyOpenSectionId}
skipLinkButton={<SkipLinkButton onClick={this.skipEditorInstall} />}
onSummaryClick={this.onStepSummaryClick}
>
{!this.isStepComplete(TutorialStep.PickEditor) ? (
<>
<p className="description">
It doesnt look like you have a text editor installed. We can
recommend{' '}
<LinkButton
uri="https://atom.io"
title="Open the Atom website"
>
Atom
</LinkButton>
{` or `}
<LinkButton
uri="https://code.visualstudio.com"
title="Open the VS Code website"
>
Visual Studio Code
</LinkButton>
, but feel free to use any.
</p>
<div className="action">
<LinkButton onClick={this.skipEditorInstall}>
I have an editor
</LinkButton>
</div>
</>
) : (
<p className="description">
Your default editor is{' '}
<strong>{this.props.resolvedExternalEditor}</strong>. You can
change your preferred editor in{' '}
<LinkButton onClick={this.onPreferencesClick}>
{__DARWIN__ ? 'Preferences' : 'options'}
</LinkButton>
</p>
)}
</TutorialStepInstructions>
<TutorialStepInstructions
summaryText="Create a branch"
isComplete={this.isStepComplete}
isNextStepTodo={this.isStepNextTodo}
sectionId={TutorialStep.CreateBranch}
currentlyOpenSectionId={this.state.currentlyOpenSectionId}
onSummaryClick={this.onStepSummaryClick}
>
<p className="description">
{`A branch allows you to work on different versions of a repository at one time. Create a
branch by going into the branch menu in the top bar and
clicking "${__DARWIN__ ? 'New Branch' : 'New branch'}".`}
</p>
<div className="action">
{__DARWIN__ ? (
<>
<kbd></kbd>
<kbd></kbd>
<kbd>N</kbd>
</>
) : (
<>
<kbd>Ctrl</kbd>
<kbd>Shift</kbd>
<kbd>N</kbd>
</>
)}
</div>
</TutorialStepInstructions>
<TutorialStepInstructions
summaryText="Edit a file"
isComplete={this.isStepComplete}
isNextStepTodo={this.isStepNextTodo}
sectionId={TutorialStep.EditFile}
currentlyOpenSectionId={this.state.currentlyOpenSectionId}
onSummaryClick={this.onStepSummaryClick}
>
<p className="description">
Open this repository in your preferred text editor. Edit the
{` `}
<Monospaced>README.md</Monospaced>
{` `}
file, save it, and come back.
</p>
{this.props.resolvedExternalEditor && (
<div className="action">
<Button onClick={this.openTutorialFileInEditor}>
{__DARWIN__ ? 'Open Editor' : 'Open editor'}
</Button>
{__DARWIN__ ? (
<>
<kbd></kbd>
<kbd></kbd>
<kbd>A</kbd>
</>
) : (
<>
<kbd>Ctrl</kbd>
<kbd>Shift</kbd>
<kbd>A</kbd>
</>
)}
</div>
)}
</TutorialStepInstructions>
<TutorialStepInstructions
summaryText="Make a commit"
isComplete={this.isStepComplete}
isNextStepTodo={this.isStepNextTodo}
sectionId={TutorialStep.MakeCommit}
currentlyOpenSectionId={this.state.currentlyOpenSectionId}
onSummaryClick={this.onStepSummaryClick}
>
<p className="description">
A commit allows you to save sets of changes. In the summary
field in the bottom left, write a short message that describes the
changes you made. When youre done, click the blue Commit button
to finish.
</p>
</TutorialStepInstructions>
<TutorialStepInstructions
summaryText="Publish to GitHub"
isComplete={this.isStepComplete}
isNextStepTodo={this.isStepNextTodo}
sectionId={TutorialStep.PushBranch}
currentlyOpenSectionId={this.state.currentlyOpenSectionId}
onSummaryClick={this.onStepSummaryClick}
>
<p className="description">
Publishing will push, or upload, your commits to this branch of
your repository on GitHub. Publish using the third button in the
top bar.
</p>
<div className="action">
{__DARWIN__ ? (
<>
<kbd></kbd>
<kbd>P</kbd>
</>
) : (
<>
<kbd>Ctrl</kbd>
<kbd>P</kbd>
</>
)}
</div>
</TutorialStepInstructions>
<TutorialStepInstructions
summaryText="Open a pull request"
isComplete={this.isStepComplete}
isNextStepTodo={this.isStepNextTodo}
sectionId={TutorialStep.OpenPullRequest}
currentlyOpenSectionId={this.state.currentlyOpenSectionId}
skipLinkButton={<SkipLinkButton onClick={this.skipCreatePR} />}
onSummaryClick={this.onStepSummaryClick}
>
<p className="description">
A pull request allows you to propose changes to the code. By
opening one, youre requesting that someone review and merge them.
Since this is a demo repository, this pull request will be
private.
</p>
<div className="action">
<Button onClick={this.openPullRequest}>
{__DARWIN__ ? 'Open Pull Request' : 'Open pull request'}
<Octicon symbol={OcticonSymbol.linkExternal} />
</Button>
{__DARWIN__ ? (
<>
<kbd></kbd>
<kbd>R</kbd>
</>
) : (
<>
<kbd>Ctrl</kbd>
<kbd>R</kbd>
</>
)}
</div>
</TutorialStepInstructions>
</ol>
<div className="footer">
<Button onClick={this.props.onExitTutorial}>
{__DARWIN__ ? 'Exit Tutorial' : 'Exit tutorial'}
</Button>
</div>
</div>
)
}
/** this makes sure we only have one `TutorialListItem` open at a time */
public onStepSummaryClick = (id: ValidTutorialStep) => {
this.setState({ currentlyOpenSectionId: id })
}
private onPreferencesClick = () => {
this.props.dispatcher.showPopup({
type: PopupType.Preferences,
initialSelectedTab: PreferencesTab.Advanced,
})
}
}
interface ITutorialStepInstructionsProps {
/** Text displayed to summarize this step */
readonly summaryText: string
/** Used to find out if this step has been completed */
readonly isComplete: (step: ValidTutorialStep) => boolean
/** The step for this section */
readonly sectionId: ValidTutorialStep
/** Used to find out if this is the next step for the user to complete */
readonly isNextStepTodo: (step: ValidTutorialStep) => boolean
/** ID of the currently expanded tutorial step
* (used to determine if this step is expanded)
*/
readonly currentlyOpenSectionId: ValidTutorialStep
/** Skip button (if possible for this step) */
readonly skipLinkButton?: JSX.Element
/** Handler to open and close section */
readonly onSummaryClick: (id: ValidTutorialStep) => void
}
/** A step (summary and expandable description) in the tutorial side panel */
class TutorialStepInstructions extends React.Component<
ITutorialStepInstructionsProps
> {
public render() {
return (
<li key={this.props.sectionId} onClick={this.onSummaryClick}>
<details
open={this.props.sectionId === this.props.currentlyOpenSectionId}
onClick={this.onSummaryClick}
>
{this.renderSummary()}
<div className="contents">{this.props.children}</div>
</details>
</li>
)
}
private renderSummary = () => {
const shouldShowSkipLink =
this.props.skipLinkButton !== undefined &&
this.props.currentlyOpenSectionId === this.props.sectionId &&
this.props.isNextStepTodo(this.props.sectionId)
return (
<summary>
{this.renderTutorialStepIcon()}
<span className="summary-text">{this.props.summaryText}</span>
<span className="hang-right">
{shouldShowSkipLink ? (
this.props.skipLinkButton
) : (
<Octicon symbol={OcticonSymbol.chevronDown} />
)}
</span>
</summary>
)
}
private renderTutorialStepIcon() {
if (this.props.isComplete(this.props.sectionId)) {
return (
<div className="green-circle">
<Octicon symbol={OcticonSymbol.check} />
</div>
)
}
// ugh zero-indexing
const stepNumber = orderedTutorialSteps.indexOf(this.props.sectionId) + 1
return this.props.isNextStepTodo(this.props.sectionId) ? (
<div className="blue-circle">{stepNumber}</div>
) : (
<div className="empty-circle">{stepNumber}</div>
)
}
private onSummaryClick = (e: React.MouseEvent<HTMLElement>) => {
// prevents the default behavior of toggling on a `details` html element
// so we don't have to fight it with our react state
// for more info see:
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details#Events
e.preventDefault()
this.props.onSummaryClick(this.props.sectionId)
}
}
const SkipLinkButton: React.SFC<{ onClick: () => void }> = props => (
<LinkButton onClick={props.onClick}>Skip</LinkButton>
)

View file

@ -0,0 +1,51 @@
import * as React from 'react'
import { encodePathAsUrl } from '../../lib/path'
const CodeImage = encodePathAsUrl(__dirname, 'static/code.svg')
const TeamDiscussionImage = encodePathAsUrl(
__dirname,
'static/github-for-teams.svg'
)
const CloudServerImage = encodePathAsUrl(
__dirname,
'static/github-for-business.svg'
)
export class TutorialWelcome extends React.Component {
public render() {
return (
<div id="tutorial-welcome">
<div className="header">
<h1>Welcome to GitHub Desktop</h1>
<p>
Use this tutorial to get comfortable with Git, GitHub, and GitHub
Desktop.
</p>
</div>
<ul className="definitions">
<li>
<img src={CodeImage} />
<p>
<strong>Git</strong> is the version control system.
</p>
</li>
<li>
<img src={TeamDiscussionImage} />
<p>
<strong>GitHub</strong> is where you store your code and
collaborate with others.
</p>
</li>
<li>
<img src={CloudServerImage} />
<p>
<strong>GitHub Desktop</strong> helps you work with GitHub
locally.
</p>
</li>
</ul>
</div>
)
}
}

View file

@ -0,0 +1,72 @@
import * as React from 'react'
import { Dialog, DialogContent, DialogFooter } from '../dialog'
import { ButtonGroup } from '../lib/button-group'
import { Button } from '../lib/button'
import { Dispatcher } from '../dispatcher'
import { Ref } from '../lib/ref'
import { Repository } from '../../models/repository'
interface IWorkflowPushRejectedDialogProps {
readonly rejectedPath: string
readonly repository: Repository
readonly dispatcher: Dispatcher
readonly onDismissed: () => void
}
interface IWorkflowPushRejectedDialogState {
readonly loading: boolean
}
/**
* The dialog shown when a push is rejected due to it modifying a
* workflow file without the workflow oauth scope.
*/
export class WorkflowPushRejectedDialog extends React.Component<
IWorkflowPushRejectedDialogProps,
IWorkflowPushRejectedDialogState
> {
public constructor(props: IWorkflowPushRejectedDialogProps) {
super(props)
this.state = { loading: false }
}
public render() {
return (
<Dialog
title={__DARWIN__ ? 'Push Rejected' : 'Push rejected'}
loading={this.state.loading}
onDismissed={this.props.onDismissed}
onSubmit={this.onSignIn}
type="error"
>
<DialogContent>
<p>
The push was rejected by the server for containing a modification to
a workflow file ( <Ref>{this.props.rejectedPath}</Ref>). In order to
be able to push to workflow files GitHub Desktop needs to request
additional permissions.
</p>
<p>
Would you like to open a browser to grant GitHub Desktop permission
to update workflow files?
</p>
</DialogContent>
<DialogFooter>
<ButtonGroup>
<Button type="submit">Grant</Button>
<Button onClick={this.props.onDismissed}>Cancel</Button>
</ButtonGroup>
</DialogFooter>
</Dialog>
)
}
private onSignIn = async () => {
this.setState({ loading: true })
await this.props.dispatcher.beginDotComSignIn()
await this.props.dispatcher.requestBrowserAuthentication()
this.props.dispatcher.push(this.props.repository)
this.props.onDismissed()
}
}

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="48" height="48" viewBox="0 0 48 48" fill="none"
xmlns="http://www.w3.org/2000/svg">
<title>Admin Mentoring</title>
<path d="M15 26C14.45 26 14 25.55 14 25V15C14 14.45 13.55 14 13 14C12.45 14 12 14.45 12 15V26.0884C12.3822 27.4316 12.3661 29.0518 12.35 30.1V30.39C12.9 30.69 13.81 31.27 14.79 32.36C15.16 32.77 15.12 33.4 14.71 33.77C14.3 34.14 13.67 34.1 13.3 33.69C12.33 32.61 11.48 32.17 11.11 31.99C10.99 31.93 10.9 31.88 10.86 31.85C10.35 31.51 10.35 31.06 10.36 30.07L10.36 30.0625C10.3686 29.1691 10.3836 27.613 10.0669 26.5586C10.0523 26.521 10.0399 26.4822 10.03 26.4426C9.98591 26.3118 9.93615 26.1901 9.87997 26.08C8.72997 23.84 7.44997 23.13 6.54997 22.94C7.61997 25.55 8.01997 28.22 8.01997 30.03C8.01997 33.78 10.1 38.99 14.01 39.01C14.56 39.01 15.01 39.46 15.01 40.01C15.01 40.58 14.56 41.02 14.01 41.02C9.07997 41 6.01997 35.31 6.01997 30.04C6.01997 28.15 5.51997 25.18 4.12997 22.5C3.99997 22.25 3.97997 21.95 4.07997 21.68C4.17997 21.41 4.38997 21.2 4.65997 21.1C5.76223 20.6808 7.99125 20.6834 10 22.7916V15C10 13.35 11.35 12 13 12C13.3502 12 13.6869 12.0608 14 12.1724V11C14 9.35 15.35 8 17 8C18.65 8 20 9.35 20 11V11.1724C20.3131 11.0608 20.6498 11 21 11C21.55 11 22.1 11.15 22.57 11.44C23.04 11.73 23.19 12.34 22.9 12.82C22.61 13.29 22 13.44 21.52 13.15C21.36 13.05 21.18 13 21 13C20.45 13 20 13.45 20 14V20.03C20 20.58 19.55 21.03 19 21.03C18.45 21.03 18 20.58 18 20.03V11C18 10.45 17.55 10 17 10C16.45 10 16 10.45 16 11V25C16 25.55 15.55 26 15 26Z" fill="#79B8FF"/>
<path d="M14.47 46.87C14.64 46.98 14.84 47.04 15.03 47.04C15.35 47.04 15.66 46.88 15.86 46.59L19.71 40.89C20.16 40.74 20.92 40.42 21.65 39.85C22.08 39.51 22.16 38.89 21.82 38.45C21.48 38.02 20.86 37.94 20.42 38.28C19.68 38.85 18.83 39.08 18.83 39.08C18.59 39.14 18.39 39.29 18.25 39.49L14.2 45.48C13.89 45.94 14.01 46.56 14.47 46.87Z" fill="#79B8FF"/>
<path d="M6.06004 45.02C5.84004 45.02 5.62004 44.95 5.44004 44.81C5.01004 44.47 4.93004 43.84 5.27004 43.41L7.64004 40.41C7.98004 39.98 8.61004 39.9 9.04004 40.24C9.47004 40.58 9.55004 41.21 9.21004 41.64L6.84004 44.64C6.65004 44.89 6.36004 45.02 6.06004 45.02Z" fill="#79B8FF"/>
<path d="M23.02 4C23.02 4.55 23.47 5 24.02 5C24.57 5 25.02 4.55 25.02 4V1C25.02 0.45 24.57 0 24.02 0C23.47 0 23.02 0.45 23.02 1V4Z" fill="#2188FF"/>
<path d="M29.32 5.75C29.51 5.94 29.77 6.04 30.02 6.04C30.28 6.04 30.54 5.94 30.73 5.74L32.72 3.72C33.11 3.33 33.1 2.7 32.71 2.31C32.32 1.92 31.69 1.92 31.3 2.32L29.31 4.34C28.92 4.73 28.93 5.36 29.32 5.75Z" fill="#2188FF"/>
<path d="M18.01 6.02001C17.75 6.02001 17.49 5.92001 17.3 5.72001L15.31 3.71001C14.92 3.32001 14.93 2.68001 15.32 2.30001C15.71 1.92001 16.35 1.91001 16.73 2.31001L18.72 4.32001C19.11 4.71001 19.1 5.35001 18.71 5.73001C18.52 5.93001 18.26 6.02001 18.01 6.02001Z" fill="#2188FF"/>
<path d="M34.2399 46.66C34.4399 46.9 34.7199 47.02 35.0099 47.02C35.2399 47.02 35.4699 46.94 35.6599 46.77C36.0799 46.41 36.1299 45.78 35.7799 45.36L30.7499 39.36C30.5499 39.12 30.2599 38.99 29.9499 39C26.6299 39.1 24.1499 36.67 23.2699 32.5C22.3799 28.25 21.7299 26.45 20.9299 24.62C21.7299 24.58 22.8999 24.89 23.9299 26.71C23.9513 26.7487 23.9747 26.7853 24 26.8198V30.06C24 30.61 24.45 31.06 25 31.06C25.55 31.06 26 30.61 26 30.06V15C26 14.45 26.45 14 27 14C27.55 14 28 14.45 28 15V25.01C28 25.56 28.45 26.01 29 26.01L29.005 26.01L29.01 26.01C29.56 26.01 30.01 25.56 30.01 25.02V13.01C30.01 12.9621 30.0066 12.915 30 12.8688V11C30 10.45 30.45 10 31 10C31.55 10 32 10.45 32 11V25C32 25.55 32.45 26 33 26C33.55 26 34 25.55 34 25V13C34 12.45 34.45 12 35 12C35.55 12 36 12.45 36 13V25C36 25.55 36.45 26 37 26C37.55 26 38 25.55 38 25V19C38 18.45 38.45 18 39 18C39.55 18 40 18.45 40 19V31.57C40 35.8 37.65 37.09 37.56 37.13C37.29 37.27 37.1 37.52 37.03 37.81C36.96 38.1 37.03 38.41 37.22 38.65L41.2 43.65C41.4 43.9 41.69 44.03 41.98 44.03C42.2 44.03 42.42 43.95 42.61 43.81C43.04 43.46 43.11 42.83 42.77 42.4L39.43 38.21C40.47 37.27 41.97 35.29 42 31.67V19C42 17.35 40.65 16 39 16C38.6498 16 38.3131 16.0608 38 16.1724V13C38 11.35 36.65 10 35 10C34.6101 10 34.2369 10.0754 33.8944 10.2123C33.5462 8.94117 32.3777 8 31 8C29.35 8 28 9.35 28 11V12.1724C27.6869 12.0608 27.3502 12 27 12C25.35 12 24 13.35 24 15V23.6912C22.1476 22.198 20.1043 22.5561 19.0799 23.11C18.6199 23.36 18.4699 24.01 18.6799 24.49L18.6819 24.4945C19.6508 26.662 20.2906 28.0933 21.3099 32.92C22.3399 37.84 25.4599 40.89 29.5099 41.01L34.2399 46.66Z" fill="#2188FF"/>
<path d="M1.75 11.64C1.87 12.07 2.26 12.37 2.71 12.37C3.16 12.37 3.56 12.07 3.68 11.63C3.78386 11.2492 3.83635 11.0567 3.95844 10.9328C4.08322 10.8062 4.28069 10.7512 4.68 10.64C5.11 10.52 5.41 10.12 5.41 9.67C5.41 9.22 5.11 8.83 4.67 8.71C4.27174 8.60414 4.07425 8.55164 3.94814 8.42695C3.82409 8.30431 3.76909 8.11182 3.66 7.73C3.54 7.3 3.15 7 2.7 7C2.25 7 1.85 7.3 1.73 7.73C1.62614 8.1108 1.57365 8.30328 1.45156 8.42718C1.32678 8.55381 1.12931 8.6088 0.73 8.72C0.3 8.84 0 9.24 0 9.69C0 10.14 0.3 10.54 0.74 10.65C1.53 10.87 1.53 10.87 1.75 11.64Z" fill="#2188FF"/>
<path d="M44.71 12.37C44.26 12.37 43.87 12.07 43.75 11.64C43.53 10.87 43.53 10.87 42.74 10.65C42.3 10.54 42 10.14 42 9.69C42 9.24 42.3 8.84 42.73 8.72C43.1293 8.6088 43.3268 8.55381 43.4516 8.42718C43.5737 8.30328 43.6261 8.1108 43.73 7.73C43.85 7.3 44.25 7 44.7 7C45.15 7.04 45.54 7.3 45.66 7.73C45.88 8.5 45.88 8.5 46.67 8.72C47.11 8.84 47.41 9.23 47.41 9.68C47.41 10.13 47.11 10.53 46.68 10.65C45.89 10.87 45.89 10.87 45.68 11.64C45.56 12.07 45.16 12.37 44.71 12.37Z" fill="#2188FF"/>
<path d="M23.0699 47.27C23.1899 47.7 23.5799 48 24.0299 48C24.4899 47.99 24.8799 47.69 24.9899 47.27C25.0938 46.8892 25.1463 46.6967 25.2684 46.5728C25.3932 46.4462 25.5906 46.3912 25.9899 46.28C26.4199 46.16 26.7199 45.76 26.7199 45.31C26.7199 44.86 26.4199 44.47 25.9799 44.35C25.1899 44.13 25.1899 44.13 24.9699 43.36C24.8499 42.93 24.4599 42.63 24.0099 42.63C23.5599 42.63 23.1699 42.93 23.0499 43.36C22.8399 44.13 22.8399 44.13 22.0499 44.35C21.6199 44.47 21.3199 44.87 21.3199 45.32C21.3199 45.77 21.6199 46.16 22.0599 46.28C22.4587 46.3911 22.6562 46.4461 22.7823 46.5723C22.906 46.6962 22.961 46.8887 23.0699 47.27Z" fill="#2188FF"/>
</svg>

After

Width:  |  Height:  |  Size: 6 KiB

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="50" height="50" viewBox="0 0 50 50" fill="none"
xmlns="http://www.w3.org/2000/svg">
<title>Code</title>
<path d="M28.96 6C29.51 6 29.96 6.45 29.96 7C29.96 7.55 29.51 8 28.96 8H19.98C19.43 8 18.98 7.55 18.98 7C18.98 6.45 19.43 6 19.98 6H28.96Z" fill="#79B8FF"/>
<path d="M22.96 43H27.96C28.51 43 28.96 43.45 28.96 44C28.96 44.55 28.51 45 27.96 45H22.96C22.41 45 21.96 44.55 21.96 44C21.96 43.45 22.41 43 22.96 43Z" fill="#79B8FF"/>
<path d="M17.96 37H5.35C4.63 35.84 3.94 34.44 3.4 33H6.96C7.51 33 7.96 32.55 7.96 32C7.96 31.45 7.51 31 6.96 31H2C1.98 31 1.9625 31.005 1.945 31.01C1.92751 31.015 1.90999 31.02 1.89 31.02C1.86209 31.024 1.83577 31.0248 1.80977 31.0256C1.77056 31.0268 1.73209 31.028 1.69 31.04L1.63 31.07C1.615 31.075 1.6025 31.0825 1.59 31.09C1.5775 31.0975 1.565 31.105 1.55 31.11C1.21 31.29 1 31.62 1 32C1 32.1 1 32.2 1.03 32.3C1.72 34.52 2.82 36.86 3.96 38.58C4.15 38.87 4.47 39.02 4.79 39.02C4.84 39.02 4.89 39.01 4.93 39H17.95C18.5 39 18.95 38.55 18.95 38C18.95 37.45 18.51 37 17.96 37Z" fill="#79B8FF"/>
<path d="M46.6 12.48C47.66 14.26 48.57 16.46 49.14 18.67C49.2 18.88 49.17 19.09 49.11 19.27C48.98 19.69 48.62 20 48.16 20H43.97C43.42 20 42.97 19.55 42.97 19C42.97 18.45 43.42 18 43.97 18H46.87C46.41 16.57 45.83 15.2 45.16 14H29.96C29.41 14 28.96 13.55 28.96 13C28.96 12.45 29.41 12 29.96 12H45.68C45.6981 12 45.7141 12.0041 45.728 12.0076C45.745 12.0119 45.759 12.0155 45.77 12.01C46.1 12.02 46.42 12.18 46.6 12.48Z" fill="#79B8FF"/>
<path d="M24.81 2.00002C22.47 2.03002 20.27 2.36002 18.3 3.00002C17.78 3.17002 17.21 2.88002 17.05 2.35002C16.88 1.82002 17.16 1.26002 17.69 1.09002C19.86 0.390018 22.25 0.0200185 24.8 1.84864e-05C30.98 -0.0399817 37.19 2.29002 41.52 6.27002C41.53 6.27002 41.53 6.28002 41.53 6.28002C41.62 6.36002 41.7 6.46002 41.75 6.58002C41.76 6.58002 41.76 6.59002 41.76 6.59002C41.81 6.69002 41.83 6.80002 41.84 6.91002C41.84 6.91416 41.8417 6.92002 41.8437 6.92688C41.8465 6.93659 41.85 6.9483 41.85 6.96002C41.85 6.97173 41.8534 6.98002 41.8562 6.98688C41.8583 6.99174 41.86 6.99588 41.86 7.00002C41.86 7.07237 41.8406 7.14472 41.8226 7.21187C41.8182 7.22825 41.8139 7.24433 41.81 7.26002C41.805 7.27502 41.8025 7.29002 41.8 7.30502C41.7975 7.32002 41.795 7.33502 41.79 7.35002C41.75 7.46002 41.69 7.55002 41.62 7.64002C41.615 7.64502 41.6125 7.65252 41.61 7.66002C41.6075 7.66752 41.605 7.67502 41.6 7.68002H41.59C41.4 7.87002 41.15 8.00002 40.86 8.00002H34C33.45 8.00002 33 7.55002 33 7.00002C33 6.45002 33.45 6.00002 34 6.00002H37.94C34.2 3.44002 29.48 1.96002 24.81 2.00002Z" fill="#2188FF"/>
<path d="M9 8H15C15.55 8 16 7.55 16 7C16 6.45 15.55 6 15 6H9C8.45 6 8 6.45 8 7C8 7.55 8.45 8 9 8Z" fill="#2188FF"/>
<path d="M25 14C25.55 14 26 13.55 26 13C26 12.45 25.55 12 25 12H4.23C4.21 12 4.195 12.005 4.18 12.01C4.165 12.015 4.15 12.02 4.13 12.02C3.8 12.04 3.49 12.2 3.31 12.51C1.11 16.45 0 20.65 0 25C0 25.55 0.45 26 1 26C1.55 26 2 25.55 2 25C2 21.18 2.95 17.49 4.79 14H25Z" fill="#2188FF"/>
<path d="M22.98 33.06C22.87 33.06 22.75 33.04 22.64 33C22.12 32.81 21.85 32.24 22.04 31.72L27.09 17.7C27.28 17.18 27.85 16.91 28.37 17.1C28.89 17.29 29.16 17.86 28.97 18.38L23.92 32.4C23.77 32.81 23.39 33.06 22.98 33.06Z" fill="#2188FF"/>
<path d="M17.71 20.29C17.32 19.9 16.69 19.9 16.3 20.29L12.9 23.69C12.18 24.41 12.18 25.59 12.9 26.31L16.3 29.71C16.49 29.9 16.74 30 17 30C17.26 30 17.51 29.9 17.71 29.71C18.1 29.32 18.1 28.69 17.71 28.3L14.41 25L17.7 21.71C18.1 21.32 18.1 20.68 17.71 20.29Z" fill="#2188FF"/>
<path d="M33.31 29.71C33.51 29.91 33.76 30 34.02 30C34.28 30 34.53 29.9 34.73 29.71L38.13 26.31C38.85 25.59 38.85 24.42 38.13 23.69L34.73 20.29C34.34 19.9 33.71 19.9 33.32 20.29C32.93 20.68 32.93 21.31 33.32 21.7L36.6 25L33.31 28.29C32.92 28.68 32.92 29.32 33.31 29.71Z" fill="#2188FF"/>
<path d="M23 39H40C40.55 39 41 38.55 41 38C41 37.45 40.55 37 40 37H23C22.45 37 22 37.45 22 38C22 38.55 22.45 39 23 39Z" fill="#2188FF"/>
<path d="M48 25.03C47.98 24.48 48.42 24.02 48.97 24C49.52 23.96 49.98 24.42 49.99 24.97C50.06 27.27 49.53 31.35 47.81 35.24C46.2 38.88 43.28 42.56 40.35 44.7C40.35 44.71 40.34 44.71 40.34 44.71C40.3199 44.7261 40.2981 44.7438 40.276 44.7619L40.26 44.775C40.2325 44.7975 40.205 44.82 40.18 44.84C40.165 44.85 40.15 44.855 40.135 44.86C40.12 44.865 40.105 44.87 40.09 44.88C40.06 44.9 40.03 44.92 40 44.93C39.87 44.98 39.74 45.02 39.61 45.02C39.595 45.02 39.58 45.015 39.565 45.01C39.55 45.005 39.535 45 39.52 45H33C32.45 45 32 44.55 32 44C32 43.45 32.45 43 33 43H39.29C41.89 41.06 44.55 37.67 45.99 34.43C47.58 30.85 48.06 27.13 48 25.03Z" fill="#2188FF"/>
<path d="M23.8 47.97C26.16 48.08 28.57 47.84 30.79 47.26C31.32 47.12 31.87 47.44 32 47.98C32.14 48.51 31.82 49.06 31.29 49.2C29.26 49.73 27.15 50 25 50C24.5785 50 24.1662 49.9908 23.7367 49.9813L23.68 49.98C21.17 49.85 18.57 49.31 16.16 48.41C13.14 47.28 10.82 45.67 9.71002 44.81C9.69274 44.7985 9.68209 44.7837 9.67044 44.7674C9.66186 44.7555 9.65274 44.7427 9.64002 44.73C9.63502 44.72 9.62752 44.7125 9.62002 44.705C9.61252 44.6975 9.60502 44.69 9.60002 44.68C9.52002 44.6 9.46002 44.52 9.42002 44.42C9.41502 44.41 9.41252 44.3975 9.41002 44.385C9.40752 44.3725 9.40502 44.36 9.40002 44.35C9.37002 44.25 9.34002 44.15 9.34002 44.05C9.34002 44.045 9.33752 44.0375 9.33502 44.03C9.33252 44.0225 9.33002 44.015 9.33002 44.01C9.33002 43.995 9.33502 43.9825 9.34002 43.97C9.34502 43.9575 9.35002 43.945 9.35002 43.93C9.36002 43.84 9.37002 43.76 9.40002 43.68C9.41502 43.65 9.43002 43.6225 9.44502 43.595C9.46002 43.5675 9.47502 43.54 9.49002 43.51C9.50002 43.495 9.50752 43.4775 9.51502 43.46C9.52252 43.4425 9.53002 43.425 9.54002 43.41C9.54879 43.4012 9.55756 43.3963 9.56548 43.3919C9.57563 43.3862 9.5844 43.3812 9.59002 43.37C9.65002 43.3 9.72002 43.25 9.79002 43.2L9.88002 43.14C9.96002 43.09 10.05 43.07 10.15 43.05C10.17 43.04 10.2 43.03 10.23 43.03C10.25 43.03 10.2675 43.025 10.285 43.02C10.3025 43.015 10.32 43.01 10.34 43.01H18C18.55 43.01 19 43.46 19 44.01C19 44.56 18.55 45.01 18 45.01H13.65C14.58 45.54 15.66 46.08 16.88 46.53C19.1 47.36 21.49 47.85 23.8 47.97Z" fill="#2188FF"/>
</svg>

After

Width:  |  Height:  |  Size: 6 KiB

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="51" height="51" viewBox="0 0 51 51" fill="none"
xmlns="http://www.w3.org/2000/svg">
<title>Explore</title>
<path d="M26 0C23.4716 0 21.0291 0.375669 18.7262 1.07492C18.1977 1.23538 17.8994 1.79387 18.0599 2.32233C18.2204 2.85079 18.7788 3.1491 19.3073 2.98864C21.4238 2.34597 23.6706 2 26 2C29.0599 2 31.9778 2.59699 34.6456 3.67992C35.1573 3.88765 35.7406 3.64121 35.9483 3.12948C36.156 2.61775 35.9096 2.03451 35.3978 1.82678C32.4952 0.648515 29.3221 0 26 0Z" fill="#79B8FF"/>
<path d="M3.98915 18.3056C4.14965 17.7772 3.85138 17.2187 3.32293 17.0582C2.79448 16.8977 2.23597 17.1959 2.07547 17.7244C1.37587 20.0278 1 22.4709 1 25C1 34.6892 6.51228 43.0893 14.5673 47.2384C15.0583 47.4913 15.6613 47.2983 15.9142 46.8073C16.1671 46.3163 15.9741 45.7133 15.4831 45.4604C8.06773 41.6407 3 33.9113 3 25C3 22.67 3.34615 20.4226 3.98915 18.3056Z" fill="#79B8FF"/>
<path d="M49.9326 17.7511C49.7727 17.2225 49.2146 16.9236 48.6859 17.0835C48.1573 17.2434 47.8584 17.8016 48.0183 18.3302C48.6565 20.4401 49 22.679 49 25C49 33.8918 43.9545 41.6068 36.5657 45.4352C36.0753 45.6892 35.8837 46.2927 36.1378 46.7831C36.3919 47.2735 36.9954 47.465 37.4858 47.211C45.5119 43.0524 51 34.6679 51 25C51 22.4807 50.627 20.0467 49.9326 17.7511Z" fill="#79B8FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.01001 10C5.01001 13.87 8.14001 17 12.01 17C15.87 17 19.01 13.87 19.01 10C19.01 6.13 15.88 3 12.01 3C8.14001 3 5.01001 6.13 5.01001 10ZM17.01 10C17.01 12.76 14.77 15 12.01 15C9.25001 15 7.01001 12.76 7.01001 10C7.01001 7.24 9.25001 5 12.01 5C14.77 5 17.01 7.24 17.01 10Z" fill="#79B8FF"/>
<path d="M28.9405 10.8329C28.8183 10.4012 28.4236 10.1038 27.975 10.1053C27.5264 10.1067 27.1336 10.4068 27.0143 10.8392C26.8617 11.3924 26.7741 11.5039 26.733 11.5448C26.6901 11.5874 26.5735 11.6761 26.0123 11.8312C25.5777 11.9513 25.2772 12.3475 25.2787 12.7984C25.2802 13.2494 25.5833 13.6435 26.0188 13.7607C26.581 13.9121 26.6982 14 26.7414 14.0425C26.7829 14.0832 26.8711 14.1942 27.0275 14.7462C27.1498 15.1778 27.5445 15.4752 27.9931 15.4737C28.4417 15.4721 28.8343 15.1721 28.9537 14.7397C29.1064 14.1862 29.1938 14.0748 29.2349 14.0339C29.2775 13.9914 29.3937 13.9028 29.9549 13.7477C30.3895 13.6276 30.69 13.2315 30.6885 12.7806C30.6871 12.3297 30.384 11.9355 29.9486 11.8182C29.3865 11.6668 29.2696 11.5791 29.2265 11.5367C29.1852 11.4962 29.0969 11.3853 28.9405 10.8329Z" fill="#79B8FF"/>
<path d="M11.891 23.4647C11.7689 23.033 11.3742 22.7355 10.9256 22.7369C10.477 22.7382 10.0842 23.0382 9.96479 23.4707C9.81213 24.0235 9.72453 24.1352 9.68328 24.1762C9.64035 24.2189 9.52389 24.3076 8.96338 24.4623C8.52868 24.5823 8.22809 24.9784 8.2295 25.4294C8.23091 25.8803 8.53398 26.2746 8.96942 26.3918C9.53125 26.5432 9.64825 26.6312 9.69145 26.6736C9.73313 26.7145 9.8214 26.8258 9.97805 27.3781C10.1004 27.8096 10.4951 28.1069 10.9437 28.1053C11.3922 28.1037 11.7848 27.8036 11.9041 27.3713C12.0568 26.8176 12.1443 26.7062 12.1854 26.6653C12.2282 26.6227 12.3445 26.5341 12.9059 26.3788C13.3405 26.2586 13.6408 25.8625 13.6393 25.4116C13.6378 24.9608 13.3348 24.5667 12.8994 24.4494C12.337 24.2979 12.2198 24.2101 12.1766 24.1676C12.1353 24.1272 12.047 24.0165 11.891 23.4647Z" fill="#79B8FF"/>
<path d="M40.8743 34.4119C40.7521 33.9802 40.3575 33.6828 39.9089 33.6842C39.4603 33.6857 39.0676 33.9857 38.9482 34.4181C38.7955 34.9712 38.708 35.0825 38.667 35.1234C38.6242 35.1659 38.508 35.2545 37.9469 35.4095C37.5124 35.5295 37.2119 35.9254 37.2131 36.3762C37.2144 36.827 37.5171 37.2212 37.9523 37.3388C38.5148 37.4908 38.6318 37.5786 38.6751 37.621C38.7163 37.6615 38.8046 37.7723 38.9608 38.3248C39.0829 38.7565 39.4776 39.0541 39.9263 39.0526C40.375 39.0512 40.7678 38.7511 40.8871 38.3186C41.0398 37.7649 41.1273 37.6535 41.1683 37.6127C41.2111 37.5702 41.3274 37.4815 41.889 37.3265C42.3239 37.2065 42.6246 36.8101 42.623 36.359C42.6214 35.9079 42.3179 35.5137 41.8822 35.3967C41.3201 35.2459 41.2032 35.1579 41.1602 35.1157C41.1188 35.0751 41.0306 34.9641 40.8743 34.4119Z" fill="#79B8FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M41.2036 2.23155C41.8025 2.06438 42.4865 1.95198 43.116 2.0113C43.5293 2.05024 43.8755 2.34044 43.9861 2.74054C44.0967 3.14064 43.9486 3.56745 43.6139 3.81306C42.6517 4.51925 42.1817 5.35705 42.0344 6.21287C41.8839 7.08792 42.0583 8.06401 42.5359 9.02774C42.9305 9.82399 43.8066 10.4778 44.8898 10.8582C45.9824 11.242 47.0529 11.2656 47.6714 11.0507C48.0306 10.9258 48.4296 11.0156 48.7008 11.2822C48.9719 11.5488 49.0684 11.9462 48.9496 12.3075C48.6856 13.1108 48.1257 13.8128 47.4991 14.3562C46.8653 14.9058 46.0974 15.3513 45.3199 15.6098C41.0399 17.0326 37.494 14.396 36.4027 11.3575C35.3046 8.29999 36.4765 4.67623 39.5192 2.92449C39.9962 2.64985 40.5951 2.40139 41.2036 2.23155ZM40.3909 4.73282C38.3105 6.01093 37.5179 8.54575 38.285 10.6815C39.0743 12.8791 41.613 14.7345 44.689 13.7119C45.071 13.5849 45.4669 13.3818 45.828 13.1273C45.287 13.0571 44.743 12.9264 44.2271 12.7452C42.8556 12.2636 41.4512 11.343 40.7439 9.91585C40.1196 8.65613 39.8266 7.25011 40.0634 5.87375C40.1306 5.48334 40.2394 5.10175 40.3909 4.73282Z" fill="#2188FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.9797 20.2808C36.0607 18.8103 34.09 18.4229 32.6828 19.4361L10.9649 35.0731C9.66358 36.01 9.33738 37.8088 10.2268 39.143C11.042 40.3658 12.6271 40.8114 13.96 40.1925L24.1314 35.4701L18.089 47.5917C17.8426 48.086 18.0435 48.6864 18.5378 48.9328C19.0321 49.1792 19.6325 48.9783 19.8789 48.484L25.0171 38.1763L25.0078 49.0105C25.0073 49.5628 25.4547 50.0109 26.007 50.0114C26.5592 50.0118 27.0073 49.5645 27.0078 49.0122L27.017 38.2587L32.1096 48.4751C32.356 48.9693 32.9564 49.1703 33.4507 48.9239C33.945 48.6775 34.1459 48.0771 33.8995 47.5828L27.1606 34.0637L38.3671 28.8607C40.0033 28.101 40.6039 26.0794 39.6478 24.5496L36.9797 20.2808ZM33.8514 21.0592C34.3205 20.7215 34.9774 20.8506 35.2837 21.3407L37.9518 25.6096C38.2705 26.1196 38.0703 26.7934 37.5249 27.0466L34.5014 28.4504L31.1591 22.9977L33.8514 21.0592ZM29.5316 24.1695L15.4016 34.3431L16.8485 36.6464L32.6753 29.2982L29.5316 24.1695ZM12.1335 36.6961L13.7753 35.514L15.0199 37.4954L13.1177 38.3785C12.6797 38.5819 12.1588 38.4355 11.8909 38.0336C11.5986 37.5952 11.7058 37.004 12.1335 36.6961Z" fill="#2188FF"/>
<path d="M20 7.1C20.41 7.17 20.75 7.27 20.98 7.38C20.79 7.63 20.15 8.24 19.09 8.91C17.42 9.96 15.06 10.97 12.45 11.71C9.78001 12.47 7.25001 12.91 5.35001 12.97C4.47001 12.99 3.77001 12.94 3.30001 12.81C3.21103 12.7878 3.14679 12.7686 3.0987 12.7543C3.06031 12.7428 3.03221 12.7344 3.01001 12.73C3.06001 12.6 3.26001 12.23 3.64001 11.82C4.01001 11.41 4.49001 11.02 5.04001 10.66C5.02001 10.44 5.01001 10.22 5.01001 10C5.01001 9.4 5.08001 8.82 5.22001 8.27C2.71001 9.48 0.900012 11.44 1.00001 12.96C1.00001 13.06 1.01001 13.16 1.03001 13.28C1.62001 15.58 6.72001 15.42 12.99 13.64C18.86 11.97 23.42 9.15 23.01 6.89C23 6.84 22.99 6.8 22.98 6.76C22.58 5.27 19.44 4.67 16.99 5.09C17.53 5.63 17.98 6.27 18.32 6.97C18.88 6.96 19.47 7 20 7.1Z" fill="#2188FF"/>
</svg>

After

Width:  |  Height:  |  Size: 6.9 KiB

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="48" height="48" viewBox="0 0 48 48" fill="none"
xmlns="http://www.w3.org/2000/svg">
<title>GitHub for Business</title>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9268 32.2105C17.8327 27.4054 22.5109 24 28 24C34.8428 24 40.4514 29.2879 40.9621 36H42C45.3023 36 48 38.6977 48 42C48 45.3023 45.3023 48 42 48H6C2.69772 48 0 45.3023 0 42C0 38.9396 2.31695 36.3985 5.28483 36.0426C6.13131 33.1319 8.81125 31 12 31C13.4582 31 14.809 31.4486 15.9268 32.2105ZM28 26C22.8995 26 18.6187 29.4789 17.376 34.1948C17.2829 34.5481 17.0045 34.8225 16.6499 34.9105C16.2953 34.9986 15.921 34.8862 15.6734 34.6175C14.7571 33.6225 13.4525 33 12 33C9.52543 33 7.47386 34.8013 7.07615 37.1659C6.99516 37.6474 6.57828 38 6.09 38H6C3.80228 38 2 39.8023 2 42C2 44.1977 3.80228 46 6 46H42C44.1977 46 46 44.1977 46 42C46 39.8023 44.1977 38 42 38H40C39.4477 38 39 37.5523 39 37C39 30.9253 34.0747 26 28 26Z" fill="#79B8FF"/>
<path d="M12 5C11.4477 5 11 5.44772 11 6C11 6.55228 11.4477 7 12 7H23C23.5523 7 24 6.55228 24 6C24 5.44772 23.5523 5 23 5H12Z" fill="#2188FF"/>
<path d="M33 4C33.5523 4 34 4.44772 34 5V7C34 7.55228 33.5523 8 33 8C32.4477 8 32 7.55228 32 7V5C32 4.44772 32.4477 4 33 4Z" fill="#2188FF"/>
<path d="M38 5C38 4.44772 37.5523 4 37 4C36.4477 4 36 4.44772 36 5V7C36 7.55228 36.4477 8 37 8C37.5523 8 38 7.55228 38 7V5Z" fill="#2188FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 3C6 1.35187 7.31071 0 9 0H39C40.6893 0 42 1.35187 42 3V9C42 10.6481 40.6893 12 39 12H35V15H39C40.6893 15 42 16.3519 42 18V24C42 25.6481 40.6893 27 39 27C38.4477 27 38 26.5523 38 26C38 25.4477 38.4477 25 39 25C39.5685 25 40 24.5599 40 24V18C40 17.4401 39.5685 17 39 17H9C8.43152 17 8 17.4401 8 18V24C8 24.5599 8.43152 25 9 25H17C17.5523 25 18 25.4477 18 26C18 26.5523 17.5523 27 17 27H9C7.31071 27 6 25.6481 6 24V18C6 16.3519 7.31071 15 9 15H14V12H9C7.31071 12 6 10.6481 6 9V3ZM16 15H33V12H16V15ZM9 2C8.43152 2 8 2.44013 8 3V9C8 9.55987 8.43152 10 9 10H39C39.5685 10 40 9.55987 40 9V3C40 2.44013 39.5685 2 39 2H9Z" fill="#2188FF"/>
<path d="M11 21C11 20.4477 11.4477 20 12 20H23C23.5523 20 24 20.4477 24 21C24 21.5523 23.5523 22 23 22H12C11.4477 22 11 21.5523 11 21Z" fill="#2188FF"/>
<path d="M34 20C34 19.4477 33.5523 19 33 19C32.4477 19 32 19.4477 32 20V22C32 22.5523 32.4477 23 33 23C33.5523 23 34 22.5523 34 22V20Z" fill="#2188FF"/>
<path d="M37 19C37.5523 19 38 19.4477 38 20V22C38 22.5523 37.5523 23 37 23C36.4477 23 36 22.5523 36 22V20C36 19.4477 36.4477 19 37 19Z" fill="#2188FF"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="48" height="48" viewBox="0 0 48 48" fill="none"
xmlns="http://www.w3.org/2000/svg">
<title>GitHub for Teams</title>
<path fill-rule="evenodd" clip-rule="evenodd" d="M30 15.0095C30 16.8829 29.141 18.5553 27.7955 19.6551C28.8417 20.0329 29.7979 20.5631 30.6216 21.2166C31.0542 21.5599 31.1267 22.1889 30.7834 22.6215C30.4401 23.0542 29.8111 23.1266 29.3785 22.7834C28.0543 21.7328 26.2465 21.0528 24.2375 21.0029C24.1587 21.006 24.0795 21.0076 24 21.0076C23.92 21.0076 23.8403 21.006 23.7611 21.0029C21.7358 21.052 19.9429 21.724 18.6256 22.7802C18.1947 23.1257 17.5653 23.0564 17.2198 22.6255C16.8743 22.1946 16.9436 21.5653 17.3745 21.2198C18.1988 20.5589 19.1532 20.0275 20.1988 19.6505C18.8566 18.5506 18 16.8803 18 15.0095C18 11.6961 20.6871 9.01141 24 9.01141C27.3129 9.01141 30 11.6961 30 15.0095ZM20 15.0095C20 12.8016 21.7907 11.0114 24 11.0114C26.2092 11.0114 28 12.8016 28 15.0095C28 17.1458 26.3234 18.8911 24.2134 19.002C24.1424 19.0007 24.0713 19 24 19C23.9285 19 23.8571 19.0006 23.7859 19.0019C21.6762 18.8907 20 17.1456 20 15.0095Z" fill="#79B8FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.0097 26.0044C19.8678 26.0044 16.5097 29.3624 16.5097 33.5044C16.5097 36.1508 17.8805 38.4771 19.9507 39.8121C16.9893 41.0577 14.7455 43.6294 14.0252 46.7769C13.902 47.3153 14.2385 47.8516 14.7769 47.9748C15.3153 48.098 15.8516 47.7615 15.9748 47.2231C16.7726 43.7369 19.9539 41.084 23.8155 41.002C23.8801 41.0036 23.9448 41.0044 24.0097 41.0044C24.0716 41.0044 24.1333 41.0037 24.1949 41.0022C28.0503 41.0887 31.2244 43.7349 32.0253 47.2237C32.1489 47.762 32.6854 48.0982 33.2237 47.9746C33.762 47.8511 34.0982 47.3145 33.9746 46.7763C33.2524 43.6303 31.015 41.0633 28.0615 39.8168C30.1357 38.4826 31.5097 36.1539 31.5097 33.5044C31.5097 29.3624 28.1517 26.0044 24.0097 26.0044ZM24.1867 39.0016C27.1423 38.9082 29.5097 36.4826 29.5097 33.5044C29.5097 30.467 27.0472 28.0044 24.0097 28.0044C20.9723 28.0044 18.5097 30.467 18.5097 33.5044C18.5097 36.4804 20.8737 38.9045 23.8261 39.0014C23.8839 39.0005 23.9419 39 24 39C24.0624 39 24.1246 39.0005 24.1867 39.0016Z" fill="#79B8FF"/>
<path d="M8 4C8.55228 4 9 4.44772 9 5V6H10C10.5523 6 11 6.44772 11 7C11 7.55228 10.5523 8 10 8H9V9C9 9.55228 8.55228 10 8 10C7.44772 10 7 9.55228 7 9V8H6C5.44772 8 5 7.55228 5 7C5 6.44772 5.44772 6 6 6H7V5C7 4.44772 7.44772 4 8 4Z" fill="#2188FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.24851 1.31539C1.92365 0.621531 2.91569 0 4 0H12C13.0843 0 14.0764 0.621531 14.7515 1.31539C15.4229 2.00539 16 2.99214 16 4V17C16 17.4045 15.7564 17.7691 15.3827 17.9239C15.009 18.0787 14.5789 17.9931 14.2929 17.7071L10.5858 14H4.98884C3.36403 13.9819 2.07545 13.4435 1.20351 12.4708C0.346826 11.5152 0 10.2603 0 9V4C0 2.99214 0.577117 2.00539 1.24851 1.31539ZM2.68192 2.71014C2.20983 3.19531 2 3.70857 2 4V9C2 9.91321 2.25067 10.6427 2.69274 11.1358C3.11891 11.6112 3.82883 11.9858 5.00583 12H11C11.2652 12 11.5196 12.1054 11.7071 12.2929L14 14.5858V4C14 3.70857 13.7902 3.19531 13.3181 2.71014C12.8497 2.22882 12.3418 2 12 2H4C3.65822 2 3.15025 2.22882 2.68192 2.71014Z" fill="#2188FF"/>
<path d="M43.7083 5.70758C44.0991 5.31731 44.0995 4.68415 43.7092 4.29336C43.319 3.90258 42.6858 3.90216 42.295 4.29242L39.0014 7.58171L37.7059 6.29079C37.3146 5.90096 36.6815 5.90208 36.2916 6.29329C35.9018 6.68451 35.9029 7.31767 36.2941 7.7075L38.2963 9.70256C38.6868 10.0917 39.3187 10.0914 39.7088 9.70178L43.7083 5.70758Z" fill="#2188FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M36 0C34.9157 0 33.9236 0.621531 33.2485 1.31539C32.5771 2.00539 32 2.99214 32 4V17C32 17.4045 32.2436 17.7691 32.6173 17.9239C32.991 18.0787 33.4211 17.9931 33.7071 17.7071L37.4142 14L43 14.0001L43.0112 13.9999C44.636 13.9818 45.9245 13.4435 46.7965 12.4708C47.6532 11.5152 48 10.2603 48 9V4C48 2.99214 47.4229 2.00539 46.7515 1.31539C46.0764 0.621531 45.0843 0 44 0H36ZM34 4C34 3.70857 34.2098 3.19531 34.6819 2.71014C35.1503 2.22882 35.6582 2 36 2H44C44.3418 2 44.8497 2.22882 45.3181 2.71014C45.7902 3.19531 46 3.70857 46 4V9C46 9.91321 45.7493 10.6427 45.3073 11.1358C44.8811 11.6112 44.1712 11.9858 42.9942 12H37C36.7348 12 36.4804 12.1054 36.2929 12.2929L34 14.5858V4Z" fill="#2188FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 27C2 23.6859 4.68673 21 8 21C11.3133 21 14 23.6859 14 27C14 28.8699 13.1446 30.5399 11.804 31.6402C12.9819 32.2073 14.0435 33.1521 14.8535 34.4789C15.1413 34.9503 14.9924 35.5658 14.521 35.8535C14.0497 36.1413 13.4342 35.9924 13.1465 35.5211C12.072 33.761 10.5093 33 8.99999 33C5.83269 33 3.37551 35.2216 2.99139 38.1309C2.91909 38.6784 2.41663 39.0637 1.86909 38.9914C1.32156 38.9191 0.936303 38.4166 1.0086 37.8691C1.33538 35.3941 2.77885 33.3066 4.84372 32.1037C3.13695 31.0459 2 29.1559 2 27ZM8 23C5.79113 23 4 24.7907 4 27C4 29.2093 5.79113 31 8 31C10.2089 31 12 29.2093 12 27C12 24.7907 10.2089 23 8 23Z" fill="#2188FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M46 27C46 29.1559 44.863 31.046 43.1562 32.1037C45.2208 33.3067 46.6641 35.3941 46.9914 37.8689C47.0638 38.4164 46.6787 38.9189 46.1311 38.9914C45.5836 39.0638 45.0811 38.6786 45.0087 38.1311C44.6238 35.2215 42.1671 33 39 33C37.4907 33 35.928 33.761 34.8535 35.5211C34.5658 35.9924 33.9504 36.1413 33.479 35.8535C33.0076 35.5658 32.8587 34.9503 33.1465 34.4789C33.9565 33.1521 35.0181 32.2073 36.196 31.6402C34.8554 30.5399 34 28.8699 34 27C34 23.6859 36.6867 21 40 21C43.3133 21 46 23.6859 46 27ZM40 23C37.7911 23 36 24.7907 36 27C36 29.2093 37.7911 31 40 31C42.2089 31 44 29.2093 44 27C44 24.7907 42.2089 23 40 23Z" fill="#2188FF"/>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="48" height="48" viewBox="0 0 48 48" fill="none"
xmlns="http://www.w3.org/2000/svg">
<title>Organized by Project Status</title>
<path d="M3 10C2.54941 10 2 10.4456 2 11.2353V12.62H5C5.55228 12.62 6 13.0677 6 13.62C6 14.1723 5.55228 14.62 5 14.62H2V44.7647C2 45.5544 2.54941 46 3 46H4.99731C5.5496 46 5.99731 46.4477 5.99731 47C5.99731 47.5523 5.5496 48 4.99731 48H3C1.24145 48 0 46.444 0 44.7647V11.2353C0 9.55599 1.24145 8 3 8H4.97851C5.5308 8 5.97851 8.44772 5.97851 9C5.97851 9.55228 5.5308 10 4.97851 10H3Z" fill="#79B8FF"/>
<path d="M43 13.0093C42.4477 13.0093 42 13.457 42 14.0093C42 14.5616 42.4477 15.0093 43 15.0093H45.9973V44.7647C45.9973 45.5544 45.4479 46 44.9973 46H43C42.4477 46 42 46.4477 42 47C42 47.5523 42.4477 48 43 48H44.9973C46.7559 48 47.9973 46.444 47.9973 44.7647V14.0831C47.9991 14.0587 48 14.0341 48 14.0093C48 13.9844 47.9991 13.9598 47.9973 13.9354V11.2353C47.9973 9.55599 46.7559 8 44.9973 8H43.0188C42.4665 8 42.0188 8.44772 42.0188 9C42.0188 9.55228 42.4665 10 43.0188 10H44.9973C45.4479 10 45.9973 10.4456 45.9973 11.2353V13.0093H43Z" fill="#79B8FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 17C13 15.3431 14.3431 14 16 14H32C33.6569 14 35 15.3431 35 17V21C35 22.6569 33.6569 24 32 24H16C14.3431 24 13 22.6569 13 21V17ZM16 16C15.4477 16 15 16.4477 15 17V21C15 21.5523 15.4477 22 16 22H32C32.5523 22 33 21.5523 33 21V17C33 16.4477 32.5523 16 32 16H16Z" fill="#2188FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 29C14.3431 29 13 30.3431 13 32V36C13 37.6569 14.3431 39 16 39H32C33.6569 39 35 37.6569 35 36V32C35 30.3431 33.6569 29 32 29H16ZM15 32C15 31.4477 15.4477 31 16 31H32C32.5523 31 33 31.4477 33 32V36C33 36.5523 32.5523 37 32 37H16C15.4477 37 15 36.5523 15 36V32Z" fill="#2188FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 0C9.34315 0 8 1.34314 8 3V45C8 46.6569 9.34315 48 11 48H37C38.6569 48 40 46.6569 40 45V3C40 1.34315 38.6569 0 37 0H11ZM10 3C10 2.44772 10.4477 2 11 2H37C37.5523 2 38 2.44772 38 3V5.00928H10V3ZM10 7.00928V45C10 45.5523 10.4477 46 11 46H37C37.5523 46 38 45.5523 38 45V7.00928H10Z" fill="#2188FF"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="46" height="46" viewBox="0 0 46 46" fill="none"
xmlns="http://www.w3.org/2000/svg">
<title>Required status check</title>
<path fill-rule="evenodd" clip-rule="evenodd" d="M37 28C32.0296 28 28 32.0286 28 37H21C20.4477 37 20 37.4477 20 38C20 38.5523 20.4477 39 21 39H28.223C29.1325 43.0079 32.7169 46 37 46C41.9705 46 46 41.9705 46 37C46 32.0286 41.9704 28 37 28ZM30 37C30 33.1333 33.134 30 37 30C40.866 30 44 33.1333 44 37C44 40.8659 40.8659 44 37 44C33.1341 44 30 40.8659 30 37Z" fill="#79B8FF"/>
<path d="M7 38C7.55231 38 8 37.5523 8 37C8 36.4477 7.55231 36 7 36C6.44769 36 6 36.4477 6 37C6 37.5523 6.44769 38 7 38Z" fill="#79B8FF"/>
<path d="M12 37C12 37.5523 11.5523 38 11 38C10.4477 38 10 37.5523 10 37C10 36.4477 10.4477 36 11 36C11.5523 36 12 36.4477 12 37Z" fill="#79B8FF"/>
<path d="M40.6726 7.71228C41.0634 7.32201 41.0638 6.68885 40.6736 6.29806C40.2833 5.90728 39.6502 5.90686 39.2594 6.29712L35.9657 9.58641L34.6702 8.29549C34.279 7.90566 33.6458 7.90678 33.256 8.29799C32.8662 8.68921 32.8673 9.32237 33.2585 9.7122L35.2606 11.7073C35.6512 12.0964 36.283 12.0961 36.6731 11.7065L40.6726 7.71228Z" fill="#2188FF"/>
<path d="M12.6736 6.29806C13.0638 6.68885 13.0634 7.32201 12.6726 7.71228L8.67313 11.7065C8.28302 12.0961 7.65118 12.0964 7.26064 11.7073L5.2585 9.7122C4.86729 9.32237 4.86617 8.68921 5.256 8.29799C5.64583 7.90678 6.27899 7.90566 6.67021 8.29549L7.96571 9.58641L11.2594 6.29712C11.6502 5.90686 12.2833 5.90728 12.6736 6.29806Z" fill="#2188FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.7769 7H28.2231C29.1328 2.99184 32.717 0 37 0C41.9706 0 46 4.02956 46 9.00079C46 13.9714 41.9704 18 37 18C34.8517 18 32.8792 17.2474 31.3318 15.9915L15.163 30.441C16.9094 32.0825 18 34.4137 18 37C18 41.9705 13.9705 46 9 46C4.0295 46 0 41.9705 0 37C0 32.0286 4.02956 28 9 28C10.6508 28 12.1978 28.4444 13.5278 29.2201L29.9262 14.5653C28.7198 13.0341 28 11.1016 28 9.00079L18 9C18 13.9706 13.9704 18 9 18C4.02963 18 0 13.9714 0 9.00079C0 4.02956 4.02943 0 9 0C13.283 0 16.8672 2.99184 17.7769 7ZM37 2C33.1344 2 30.0004 5.13357 30 9C30 12.8658 33.1339 16 37 16C40.8661 16 44 12.8666 44 9.00079C44 5.134 40.8659 2 37 2ZM16 9C15.9996 5.13357 12.8656 2 9 2C5.13413 2 2 5.134 2 9.00079C2 12.8666 5.13394 16 9 16C12.8661 16 16 12.8658 16 9ZM9 30C5.134 30 2 33.1333 2 37C2 40.8659 5.13407 44 9 44C12.8659 44 16 40.8659 16 37C16 33.1333 12.866 30 9 30Z" fill="#2188FF"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -68,6 +68,12 @@ body {
}
}
.selectable-text {
-webkit-user-select: text;
user-select: text;
cursor: auto;
}
img {
-webkit-user-drag: none;
user-select: none;

View file

@ -72,3 +72,5 @@
@import 'ui/cloneable-repository-filter-list';
@import 'ui/stash-diff-viewer';
@import 'ui/commit-details';
@import 'ui/onboarding-tutorial/right-panel';
@import 'ui/onboarding-tutorial/welcome';

View file

@ -39,6 +39,12 @@ $overlay-background-color: rgba(0, 0, 0, 0.4);
--secondary-button-focus-shadow-color: #{rgba($gray-200, 0.75)};
--secondary-button-focus-border-color: #{$gray-300};
/**
* Color for icons that are placed on top of a colored backing
* (like a circle badge icon)
*/
--badge-icon-color: #{$white};
// Typography
//
// Font, line-height, and color for body text, headings, and more.
@ -184,6 +190,7 @@ $overlay-background-color: rgba(0, 0, 0, 0.4);
--spacing-double: calc(var(--spacing) * 2);
--spacing-triple: calc(var(--spacing) * 3);
--spacing-quad: calc(var(--spacing) * 4);
--spacing-quint: calc(var(--spacing) * 5);
--spacing-half: calc(var(--spacing) / 2);
--spacing-third: calc(var(--spacing) / 3);
@ -278,6 +285,11 @@ $overlay-background-color: rgba(0, 0, 0, 0.4);
--popup-z-index: 10;
--popup-overlay-z-index: calc(var(--popup-z-index) - 1);
--foldout-z-index: calc(var(--popup-z-index) - 2);
/**
* This is 7 to make sure it appears on top code mirror
* but behind popups, overlays, and foldouts
*/
--side-panel-z-index: calc(var(--popup-z-index) - 3);
/**
* Toast notifications are shown temporarily for things like the zoom
@ -304,6 +316,7 @@ $overlay-background-color: rgba(0, 0, 0, 0.4);
*/
--primary-blankslate-action-background: #{$blue-000};
--primary-blankslate-action-border-color: #{$blue-200};
--blankslate-action-icon-color: #{$blue-400};
/**
* Diff view

View file

@ -30,6 +30,12 @@ body.theme-dark {
--secondary-button-focus-shadow-color: #{rgba($gray-200, 0.75)};
--secondary-button-focus-border-color: #{$gray-300};
/**
* Color for icons that are placed on top of a colored backing
* (like a circle badge icon)
*/
--badge-icon-color: #{$gray-300};
/**
* Background color for custom scroll bars.
* The color is applied to the thumb part of the scrollbar.
@ -225,6 +231,7 @@ body.theme-dark {
*/
--primary-blankslate-action-background: #{$blue-900};
--primary-blankslate-action-border-color: #{$blue-700};
--blankslate-action-icon-color: #{$blue-400};
/**
* Diff view

View file

@ -1,7 +1,6 @@
@import 'changes/commit-message';
@import 'changes/continue-rebase';
@import 'changes/changes-list';
@import 'changes/sidebar';
@import 'changes/undo-commit';
@import 'changes/changes-view';
@import 'changes/no-changes';

View file

@ -11,6 +11,7 @@
@import 'dialogs/usage-reporting';
@import 'dialogs/stash-changes';
@import 'dialogs/commit-conflicts-warning';
@import 'dialogs/create-tutorial-repository';
// The styles herein attempt to follow a flow where margins are only applied
// to the bottom of elements (with the exception of the last child). This to

View file

@ -29,6 +29,21 @@
margin-right: var(--spacing-double);
}
.image-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
margin-right: var(--spacing-double);
flex-shrink: 0;
.octicon {
height: 100%;
width: 100%;
color: var(--blankslate-action-icon-color);
}
}
h2 {
margin: 0;
padding: 0;

View file

@ -1,6 +1,6 @@
#no-changes {
padding: var(--spacing-quad);
width: 100%;
flex: 1 1 700px;
min-height: 100%;
display: flex;
flex-direction: column;
@ -54,6 +54,11 @@
}
}
ul.actions {
// override default ul styles
padding: 0;
}
.action-leave {
opacity: 1;
transform: translate3d(0, 0, 0);

View file

@ -1,6 +0,0 @@
#changes-sidebar-contents {
flex-grow: 1;
display: flex;
flex-direction: column;
min-width: 0;
}

View file

@ -0,0 +1,17 @@
dialog#create-tutorial-repository-dialog {
width: 450px;
progress {
width: 100%;
}
.progress-container {
margin-top: var(--spacing);
.description {
font-family: var(--font-family-monospace);
font-size: var(--font-size-sm);
color: var(--text-secondary-color);
}
}
}

View file

@ -0,0 +1,149 @@
.tutorial-panel-component {
display: flex;
flex-flow: column nowrap;
flex: 1 1 350px;
min-width: 264px;
max-width: 350px;
height: 100%;
background-color: var(--background-color);
box-shadow: var(--base-box-shadow);
// we need this to be high so the panel's box shadow
// appears on top of the codemirror buffer
z-index: var(--side-panel-z-index);
overflow-y: scroll;
.titleArea {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-triple);
padding-left: var(--spacing-quint);
padding-right: var(--spacing-double);
h3 {
margin: 0;
font-weight: var(--font-weight-light);
font-size: var(--font-size-lg);
}
svg {
height: 46px;
width: 46px;
}
}
ol,
li,
summary {
list-style: none;
padding: 0;
margin: 0;
}
summary::-webkit-details-marker {
display: none;
}
li,
.titleArea {
border-bottom: var(--base-border);
}
li {
padding: var(--spacing-double);
}
summary {
display: flex;
align-items: center;
font-weight: var(--font-weight-semibold);
.summary-text {
opacity: 0.5;
}
.hang-right {
margin-left: auto;
}
.green-circle {
@extend %circle;
background-color: $green;
border-color: $green;
color: var(--badge-icon-color);
}
.blue-circle {
@extend %circle;
background-color: $blue;
border-color: $blue;
color: var(--badge-icon-color);
}
.empty-circle {
@extend %circle;
background-color: transparent;
border-color: var(--text-color);
opacity: 0.5;
color: var(--text-color);
}
}
details .contents {
padding-left: var(--spacing-triple);
.description {
margin-bottom: var(--spacing);
}
.action {
display: flex;
align-items: center;
.button-component {
display: flex;
align-items: center;
margin-right: var(--spacing);
.octicon {
margin-left: var(--spacing);
color: var(--text-secondary-color);
height: 14px;
width: 14px;
}
}
kbd {
// get these to align properly in a flex box
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary-color);
}
}
}
details[open] summary {
margin-bottom: var(--spacing);
.summary-text {
opacity: 1;
}
// we only want to do this if its an (chevron) octicon
.hang-right .octicon {
transform: rotate(180deg);
}
.empty-circle {
opacity: 1;
}
}
.footer {
display: flex;
justify-content: center;
padding: var(--spacing-double) 0;
margin-top: auto;
}
}
%circle {
border-radius: 50%;
border-style: solid;
border-width: 1px;
height: 18px;
width: 18px;
display: flex;
justify-content: center;
align-items: center;
margin-right: var(--spacing);
flex-shrink: 0;
flex-grow: 0;
}

View file

@ -0,0 +1,48 @@
#tutorial-welcome {
display: flex;
flex-flow: column nowrap;
align-items: center;
flex: 1 1 630px;
height: 100%;
padding: 0 var(--spacing-triple);
.header {
width: 100%;
display: flex;
flex-flow: column nowrap;
align-items: center;
margin: var(--spacing-quint) 0;
text-align: center;
h1 {
font-weight: var(--font-weight-light);
font-size: var(--font-size-xxl);
line-height: 1.1;
}
p {
margin: var(--spacing-half) 0;
padding: 0;
}
}
.definitions {
display: flex;
justify-content: space-around;
padding: 0;
max-width: 600px;
flex-wrap: wrap;
li {
display: flex;
flex-flow: column nowrap;
flex: 0 1 160px;
padding: 0 var(--spacing-half) var(--spacing-double);
img {
align-self: center;
}
p {
text-align: start;
}
}
}
}

View file

@ -6,6 +6,8 @@ const environmentVariables = {
GIT_COMMITTER_EMAIL: 'joe.bloggs@somewhere.com',
// signalling to dugite to use the bundled Git environment
TEST_ENV: '1',
HOME: '',
USERPROFILE: '',
}
process.env = { ...process.env, ...environmentVariables }

View file

@ -1,9 +1,24 @@
import { lookupPreferredEmail } from '../../src/lib/email'
import { IAPIEmail } from '../../src/lib/api'
import {
IAPIEmail,
getDotComAPIEndpoint,
getEnterpriseAPIURL,
} from '../../src/lib/api'
import { Account } from '../../src/models/account'
describe('emails', () => {
it('returns null for empty list', () => {
expect(lookupPreferredEmail([])).toBeNull()
const account = new Account(
'shiftkey',
getDotComAPIEndpoint(),
'',
[],
'',
-1,
'Caps Lock'
)
expect(lookupPreferredEmail(account)).toBeNull()
})
it('returns the primary if it has public visibility', () => {
@ -28,7 +43,17 @@ describe('emails', () => {
},
]
const result = lookupPreferredEmail(emails)
const account = new Account(
'shiftkey',
getDotComAPIEndpoint(),
'',
emails,
'',
-1,
'Caps Lock'
)
const result = lookupPreferredEmail(account)
expect(result).not.toBeNull()
expect(result!.email).toBe('my-primary-email@example.com')
})
@ -55,7 +80,17 @@ describe('emails', () => {
},
]
const result = lookupPreferredEmail(emails)
const account = new Account(
'shiftkey',
getDotComAPIEndpoint(),
'',
emails,
'',
-1,
'Caps Lock'
)
const result = lookupPreferredEmail(account)
expect(result).not.toBeNull()
expect(result!.email).toBe('my-primary-email@example.com')
})
@ -82,11 +117,58 @@ describe('emails', () => {
},
]
const result = lookupPreferredEmail(emails)
const account = new Account(
'shiftkey',
getDotComAPIEndpoint(),
'',
emails,
'',
-1,
'Caps Lock'
)
const result = lookupPreferredEmail(account)
expect(result).not.toBeNull()
expect(result!.email).toBe('shiftkey@users.noreply.github.com')
})
it('returns the noreply if there is no public address for GitHub Enterprise Server as well', () => {
const emails: IAPIEmail[] = [
{
email: 'shiftkey@example.com',
primary: false,
verified: true,
visibility: null,
},
{
email: 'shiftkey@users.noreply.github.example.com',
primary: false,
verified: true,
visibility: null,
},
{
email: 'my-primary-email@example.com',
primary: true,
verified: true,
visibility: 'private',
},
]
const account = new Account(
'shiftkey',
getEnterpriseAPIURL('https://github.example.com'),
'',
emails,
'',
-1,
'Caps Lock'
)
const result = lookupPreferredEmail(account)
expect(result).not.toBeNull()
expect(result!.email).toBe('shiftkey@users.noreply.github.example.com')
})
it('uses first email if nothing special found', () => {
const emails: IAPIEmail[] = [
{
@ -103,7 +185,17 @@ describe('emails', () => {
},
]
const result = lookupPreferredEmail(emails)
const account = new Account(
'shiftkey',
getDotComAPIEndpoint(),
'',
emails,
'',
-1,
'Caps Lock'
)
const result = lookupPreferredEmail(account)
expect(result).not.toBeNull()
expect(result!.email).toBe('shiftkey@example.com')
})

View file

@ -73,7 +73,7 @@ describe('git/stash', () => {
it('creates a stash entry when repo is not unborn or in any kind of conflict or rebase state', async () => {
await FSE.appendFile(readme, 'just testing stuff')
await createDesktopStashEntry(repository, 'master')
await createDesktopStashEntry(repository, 'master', [])
const stash = await getStashes(repository)
const entries = stash.desktopEntries
@ -92,7 +92,11 @@ describe('git/stash', () => {
expect(files).toHaveLength(1)
expect(files[0].status.kind).toBe(AppFileStatusKind.Untracked)
await createDesktopStashEntry(repository, 'master')
const untrackedFiles = status.workingDirectory.files.filter(
f => f.status.kind === AppFileStatusKind.Untracked
)
await createDesktopStashEntry(repository, 'master', untrackedFiles)
status = await getStatusOrThrow(repository)
files = status.workingDirectory.files

View file

@ -311,6 +311,78 @@ describe('Tokenizer', () => {
expect(results[12].text).toBe(`!`)
})
it('renders multiple issue links and mentions even with commas', () => {
const firstId = 3174
const firstExpectedUrl = `${htmlURL}/issues/${firstId}`
const secondId = 3184
const secondExpectedUrl = `${htmlURL}/issues/${secondId}`
const thirdId = 3207
const thirdExpectedUrl = `${htmlURL}/issues/${thirdId}`
const text =
'Assorted changelog typos - #3174, #3184 & #3207. Thanks @strafe, @alanaasmaa, and @jt2k!'
const tokenizer = new Tokenizer(emoji, repository)
const results = tokenizer.tokenize(text)
expect(results).toHaveLength(13)
expect(results[0].kind).toBe(TokenType.Text)
expect(results[0].text).toBe('Assorted changelog typos - ')
expect(results[1].kind).toBe(TokenType.Link)
const firstIssueLink = results[1] as HyperlinkMatch
expect(firstIssueLink.text).toBe('#3174')
expect(firstIssueLink.url).toBe(firstExpectedUrl)
expect(results[2].kind).toBe(TokenType.Text)
expect(results[2].text).toBe(', ')
expect(results[3].kind).toBe(TokenType.Link)
const secondIssueLink = results[3] as HyperlinkMatch
expect(secondIssueLink.text).toBe('#3184')
expect(secondIssueLink.url).toBe(secondExpectedUrl)
expect(results[4].kind).toBe(TokenType.Text)
expect(results[4].text).toBe(' & ')
expect(results[5].kind).toBe(TokenType.Link)
const thirdIssueLink = results[5] as HyperlinkMatch
expect(thirdIssueLink.text).toBe('#3207')
expect(thirdIssueLink.url).toBe(thirdExpectedUrl)
expect(results[6].kind).toBe(TokenType.Text)
expect(results[6].text).toBe('. Thanks ')
expect(results[7].kind).toBe(TokenType.Link)
const firstUserLink = results[7] as HyperlinkMatch
expect(firstUserLink.text).toBe('@strafe')
expect(firstUserLink.url).toBe('https://github.com/strafe')
expect(results[8].kind).toBe(TokenType.Text)
expect(results[8].text).toBe(', ')
expect(results[9].kind).toBe(TokenType.Link)
const secondUserLink = results[9] as HyperlinkMatch
expect(secondUserLink.text).toBe('@alanaasmaa')
expect(secondUserLink.url).toBe('https://github.com/alanaasmaa')
expect(results[10].kind).toBe(TokenType.Text)
expect(results[10].text).toBe(', and ')
expect(results[11].kind).toBe(TokenType.Link)
const thirdUserLink = results[11] as HyperlinkMatch
expect(thirdUserLink.text).toBe('@jt2k')
expect(thirdUserLink.url).toBe('https://github.com/jt2k')
expect(results[12].kind).toBe(TokenType.Text)
expect(results[12].text).toBe(`!`)
})
it('converts full URL to issue shorthand', () => {
const text = `Note: we keep a "black list" of authentication methods for which we do
not want to enable http.emptyAuth automatically. A white list would be

View file

@ -161,13 +161,13 @@ export const highlighter = merge({}, commonConfig, {
modes: {
enforce: true,
name: (mod, chunks) => {
const builtInMode = /node_modules\/codemirror\/mode\/(\w+)\//.exec(
const builtInMode = /node_modules[\\\/]codemirror[\\\/]mode[\\\/](\w+)[\\\/]/i.exec(
mod.resource
)
if (builtInMode) {
return `mode/${builtInMode[1]}`
}
const external = /node_modules\/codemirror-mode-(\w+)\//.exec(
const external = /node_modules[\\\/]codemirror-mode-(\w+)[\\\/]/i.exec(
mod.resource
)
if (external) {

View file

@ -16,6 +16,7 @@ branches:
- development
- /releases\/.+/
- /^__release-.*/
- /epic\/.*/
skip_tags: true

View file

@ -1,5 +1,61 @@
{
"releases": {
"2.2.1-beta0": [],
"2.2.0": [
"[New] Interactive tutorial for new users to become productive using Git and GitHub more quickly - #8148 #8149",
"[Added] Support pushing workflow files for GitHub Actions to GitHub.com - #7079",
"[Added] Enforce web flow authentication for users who are part of orgs using single sign-on - #8327",
"[Added] Support CodeRunner as an external editor - #8091. Thanks @ns-ccollins!",
"[Added] Support VSCodium as an external editor - #8000. Thanks @Rexogamer!",
"[Fixed] Commit description shadow visibility updates when typing - #7994. Thanks @KarstenRa!",
"[Fixed] Commit summaries with comma delimited issues are not parsed - #8162. Thanks @say25!",
"[Fixed] File path truncation in merge conflicts dialog - #6666",
"[Fixed] Git configuration fields in onboarding were not pre-filled from user's profile - #8323",
"[Fixed] Keep conflicting untracked files when bringing changes to another branch - #8084 #8200",
"[Fixed] Make app's version selectable in \"About\" dialog - #8334",
"[Improved] Application menu bar is visible when no repositories have been added to the app - #8209",
"[Improved] Support stashing lots of untracked files on Windows - #8345",
"[Improved] Surface errors from branch creation to user - #8306 #5997 #8106"
],
"2.2.0-beta3": [
"[Fixed] File path truncation in merge conflicts dialog - #6666",
"[Fixed] Support pushing workflow files for GitHub Actions to GitHub.com - #7079",
"[Fixed] Resume tutorial if new repo has been added - #8341",
"[Improved] Retain commit summary placeholder in tutorial repo - #8354",
"[Improved] Tutorial welcome design revisions - #8344",
"[Improved] Support stashing lots of untracked files on Windows - #8345",
"[Improved] Specific error message when tutorial creation fails due to repository already existing - #8351",
"[Improved] Tweak responsive styles for welcome pane - #8352",
"[Improved] Tutorial step instructions are more clear - #8374"
],
"2.2.0-beta2": [
"[Added] Add \"Welcome\" and \"You're done!\" screens to onboarding tutorial - #8232",
"[Added] Button to exit and resume onboarding tutorial - #8231",
"[Added] Enforce web flow authentication for users who are part of orgs using single sign-on - #8327",
"[Fixed] Git configuration fields in onboarding were not pre-filled from user's profile - #8323",
"[Fixed] Make app's version selectable in \"About\" dialog - #8334",
"[Fixed] Keep conflicting untracked files when bringing changes to another branch - #8084 #8200",
"[Fixed] Mark pull request step in tutorial done more quickly - #8317",
"[Fixed] Prevent triggering multiple tutorial creation flows in parallel - #8288",
"[Fixed] Surface errors from branch creation to user - #8306 #5997 #8106",
"[Fixed] Reliably show \"Open editor\" button in tutorial after editor has been installed - #8315"
],
"2.2.0-beta1": [
"[New] Interactive tutorial for new users to become productive using Git and GitHub more quickly - #8148 #8149",
"[Added] Support CodeRunner as an external editor - #8091. Thanks @ns-ccollins!",
"[Added] Support VSCodium as an external editor - #8000. Thanks @Rexogamer!",
"[Fixed] Commit summaries with comma delimited issues are parsed - #8162. Thanks @say25!",
"[Fixed] Commit description shadow visibility updates when typing - #7994. Thanks @KarstenRa!",
"[Improved] Application menu bar is visible when no repositories have been added to the app - #8209"
],
"2.1.3": [
"[Fixed] Changes from remote branch erroneously displayed on corresponding branch on Desktop - #8155 #8167",
"[Fixed] Sign-in flow for Windows users not possible via OAuth - #8154 #8142"
],
"2.1.3-beta1": [
"[Fixed] Changes from remote branch erroneously displayed on corresponding branch on Desktop - #8155 #8167",
"[Fixed] Sign-in flow for Windows users not possible via OAuth - #8154 #8142"
],
"2.1.2": [
"[Added] Syntax highlighting support for 20 more programming languages - #7217. Thanks @KennethSweezy!",
"[Added] Kitty shell support for macOS - #5162",

View file

@ -28,7 +28,7 @@ Details about how the team is organizing and shipping GitHub Desktop:
releases
- **[Issue Triage](process/issue-triage.md)** - how we address issues reported
by users
- **[Pull Request Triage](process/pull-request-triage.md)** - how contributions are reviewed
- **[Pull Requests](process/pull-requests.md)** - how code contributions are submitted and reviewed
- **[Releasing Updates](process/releasing-updates.md)** - how we deploy things
## Technical

View file

@ -22,7 +22,7 @@ You can download Node from the [Node.js website](https://nodejs.org/), install t
If you see the output `v10.x.y` or later, you're good to go.
**Node.js installation notes:**
- make sure you allow the Node.js installer to add `node` to the PATH.
- make sure you allow the Node.js installer to add `node` to the `PATH`.
### I need to use different versions of Node.js in different projects!
@ -32,7 +32,7 @@ We currently support `nvm`.
1. Install `nvm` using the instructions [here](https://github.com/coreybutler/nvm-windows).
2. Within the Desktop source directory, install version of Node.js it requires:
2. Within the Desktop source directory, install the version of Node.js it requires:
```shellsession
$ nvm install
@ -89,7 +89,7 @@ don't have any Node tools installed. You can install Python 2.7 from the
## Visual C++ Build Tools
To build native Node modules, you will need a recent version of Visual C++ which
can be obtained in several ways
can be obtained in several ways:
### Visual Studio 2017
@ -100,7 +100,7 @@ workload included.
<img width="1265" src="https://user-images.githubusercontent.com/359239/48849855-a2091800-ed7d-11e8-950b-93465eba7cd1.png">
Once you've confirmed that, open a shell and run this command to update the
configuration of NPM::
configuration of NPM:
```shellsession
$ npm config set msvs_version 2017

View file

@ -65,7 +65,10 @@ problems.
## Running tests
- `yarn test` - Runs all unit and integration tests
- `yarn test:unit` - Runs all unit tests (add `--debug` to open Chrome Dev Tools while running tests)
- `yarn test:unit` - Runs all unit tests
- Add `<file>` or `<pattern>` argument to only run tests in the specified file or files matching a pattern
- Add `-t <regex>` to only match tests whose name matches a regex
- For more information on these and other arguments, see [Jest CLI options](https://jestjs.io/docs/en/23.x/cli)
- `yarn test:integration` - Runs all integration tests
## Debugging

View file

@ -11,7 +11,7 @@ Download the `GitHub Desktop.zip`, unpack the application and put it wherever yo
On Windows you have two options:
- Download the `GitHubDesktopSetup.exe` and run it to install it for the current user.
- Download the `GitHubDesktopSetup.msi` and run it to install a machine-wide version of GitHub Desktop - each logged-in user will then be able to run GitHub Desktop from the program at `%PROGRAMFILES(x86)\GitHub Desktop Installer\desktop.exe`
- Download the `GitHubDesktopSetup.msi` and run it to install a machine-wide version of GitHub Desktop - each logged-in user will then be able to run GitHub Desktop from the program at `%PROGRAMFILES(x86)\GitHub Desktop Installer\desktop.exe`.
## Data Directories

View file

@ -2,59 +2,61 @@
The following are the larger areas of upcoming work the GitHub Desktop team intends to explore. This is not inclusive of everything we're working on, and it's not written in stone. We'll continue to update it as our priorities evolve.
#### Branch list grows with merged & deleted branches making it difficult to find those you care about (in progress)
#### New user tutorial to get started more easily doing a full workflow in GitHub Desktop
- Prune branches after they've been deleted: [#750](https://github.com/desktop/desktop/issues/750)
- Measuring success: N/A for now
- Tutorial entry point: [#8148](https://github.com/desktop/desktop/issues/8148)
- GitHub workflow tutorial using GitHub Desktop: [#8149](https://github.com/desktop/desktop/issues/8149)
#### Users behind corporate proxies cannot clone without manual setup
#### Branch protection on your desktop
- Help prevent people from making commits to branches they aren't able to push to: [#7023](https://github.com/desktop/desktop/issues/7023)
#### Rebase conflict detection
- Warn users if there will be conflicts prior to starting a rebase: [#6960](https://github.com/desktop/desktop/issues/6960)
#### Users behind corporate proxies cannot clone repositories in Desktop without manual setup
- Help people get set up correctly if they're behind a proxy: [#2789](https://github.com/desktop/desktop/issues/2789)
- Measuring success: TBD alongside the work
## Shipped in previous releases
#### Branch list grows with merged & deleted branches making it difficult to find those you care about (2.1)
- Prune branches after they've been deleted: [#750](https://github.com/desktop/desktop/issues/750)
#### Working with uncommitted changes, aka stashing (2.0)
- Improve workflows when you have uncommitted changes: [#6107](https://github.com/desktop/desktop/issues/6107)
- Measuring success: TBD alongside the work + usability testing
#### Support rebase when pulling (2.0)
- Respect user's git config for pull --rebase and resolve conflicts: [#3422](https://github.com/desktop/desktop/issues/3422)
- Measuring success: [#6550](https://github.com/desktop/desktop/issues/6550)
#### Support full rebase story, including rebasing one branch onto another locally (2.0)
- Improve rebase workflows: [#5953](https://github.com/desktop/desktop/issues/5953)
- Measuring success: TBD alongside the work + usability testing
#### Repositories are difficult to find, navigate, and differentiate (2.0)
- Help people navigate between and visually differentiate between repos: [#6460](https://github.com/desktop/desktop/issues/6460)
- Measuring success: TBD alongside the work + usability testing
#### Onboarding (1.6)
- Improve onboarding for new users: [#5686](https://github.com/desktop/desktop/issues/5686)
- Measuring success: [#5549](https://github.com/desktop/desktop/issues/5549) + usability testing
#### Suggested next steps (1.6)
- Suggest logical next steps based on what state a person's repository is in: [#6445](https://github.com/desktop/desktop/pull/6445)
- Measuring success: [#6714](https://github.com/desktop/desktop/issues/6714)
#### Merge conflicts iteration (1.6)
- Iterate on initial merge conflicts ship: [#6213](https://github.com/desktop/desktop/issues/6213)
- Measuring success: [#6388](https://github.com/desktop/desktop/issues/6388)
#### Merge conflicts handling (1.5)
- Improve how Desktop handles merge conflicts: [#5400](https://github.com/desktop/desktop/issues/5400)
- Measuring success: [#5394](https://github.com/desktop/desktop/issues/5394)
#### Merge workflow iteration (1.5)
- Evaluate and improve merge flow end-to-end: [#5555](https://github.com/desktop/desktop/issues/5555)
- Usability testing for merging

Some files were not shown because too many files have changed in this diff Show more