Merge branch 'development' into releases/2.8.1

This commit is contained in:
Sergio Padrino 2021-05-05 11:34:15 +02:00 committed by GitHub
commit d82c1431aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 556 additions and 58 deletions

2
.nvmrc
View file

@ -1 +1 @@
v14
v14.15.4

View file

@ -27,6 +27,7 @@ Want to test out new features and get fixes before everyone else? Install the
beta channel to get access to early builds of Desktop:
- [macOS](https://central.github.com/deployments/desktop/desktop/latest/darwin?env=beta)
- [macOS (Apple Silicon)](https://central.github.com/deployments/desktop/desktop/latest/darwin-arm64?env=beta)
- [Windows](https://central.github.com/deployments/desktop/desktop/latest/win32?env=beta)
- [Windows (ARM64)](https://central.github.com/deployments/desktop/desktop/latest/win32-arm64?env=beta)

View file

@ -17,7 +17,7 @@
},
"license": "MIT",
"dependencies": {
"app-path": "^3.2.0",
"app-path": "^3.3.0",
"byline": "^5.0.0",
"chalk": "^2.3.0",
"classnames": "^2.2.5",
@ -25,20 +25,20 @@
"codemirror-mode-elixir": "^1.1.2",
"compare-versions": "^3.6.0",
"deep-equal": "^1.0.1",
"desktop-trampoline": "desktop/desktop-trampoline#v0.9.4",
"desktop-trampoline": "desktop/desktop-trampoline#v0.9.7",
"dexie": "^2.0.0",
"double-ended-queue": "^2.1.0-0",
"dugite": "^1.102.0",
"dugite": "^1.103.0",
"electron-window-state": "^5.0.3",
"event-kit": "^2.0.0",
"file-metadata": "^1.0.0",
"file-uri-to-path": "^2.0.0",
"file-url": "^2.0.2",
"focus-trap-react": "^8.1.0",
"fs-admin": "^0.15.0",
"fs-admin": "^0.19.0",
"fs-extra": "^9.0.1",
"fuzzaldrin-plus": "^0.6.0",
"keytar": "^7.2.0",
"keytar": "^7.7.0",
"mem": "^4.3.0",
"memoize-one": "^4.0.3",
"moment": "^2.24.0",
@ -53,7 +53,7 @@
"react-dom": "^16.8.4",
"react-transition-group": "^4.4.1",
"react-virtualized": "^9.20.0",
"registry-js": "^1.12.0",
"registry-js": "^1.15.0",
"source-map-support": "^0.4.15",
"strip-ansi": "^4.0.0",
"textarea-caret": "^3.0.2",

View file

@ -78,7 +78,7 @@ export function enableTextDiffExpansion(): boolean {
/** Should we allow apps running from Rosetta to auto-update to ARM64 builds? */
export function enableUpdateFromRosettaToARM64(): boolean {
return false
return enableBetaFeatures()
}
/** Should we allow using the save dialog when choosing where to clone a repo */

View file

@ -64,7 +64,7 @@ function mapStatus(
export async function getCommits(
repository: Repository,
revisionRange: string,
limit: number,
limit?: number,
additionalArgs: ReadonlyArray<string> = []
): Promise<ReadonlyArray<Commit>> {
const { formatArgs, parse } = createLogParser({
@ -82,22 +82,22 @@ export async function getCommits(
refs: '%D',
})
const result = await git(
[
'log',
revisionRange,
`--date=raw`,
`--max-count=${limit}`,
...formatArgs,
'--no-show-signature',
'--no-color',
...additionalArgs,
'--',
],
repository.path,
'getCommits',
{ successExitCodes: new Set([0, 128]) }
const args = ['log', revisionRange, `--date=raw`]
if (limit !== undefined) {
args.push(`--max-count=${limit}`)
}
args.push(
...formatArgs,
'--no-show-signature',
'--no-color',
...additionalArgs,
'--'
)
const result = await git(args, repository.path, 'getCommits', {
successExitCodes: new Set([0, 128]),
})
// if the repository has an unborn HEAD, return an empty history of commits
if (result.exitCode === 128) {

View file

@ -32,6 +32,7 @@ import { stageFiles } from './update-index'
import { getStatus } from './status'
import { getCommitsBetweenCommits } from './rev-list'
import { Branch } from '../../models/branch'
import { getCommits } from './log'
/** The app-specific results from attempting to rebase a repository */
export enum RebaseResult {
@ -433,7 +434,8 @@ export async function continueRebase(
repository: Repository,
files: ReadonlyArray<WorkingDirectoryFileChange>,
manualResolutions: ReadonlyMap<string, ManualConflictResolution> = new Map(),
progressCallback?: (progress: IRebaseProgress) => void
progressCallback?: (progress: IRebaseProgress) => void,
gitEditor: string = ':'
): Promise<RebaseResult> {
const trackedFiles = files.filter(f => {
return f.status.kind !== AppFileStatusKind.Untracked
@ -478,7 +480,7 @@ export async function continueRebase(
GitError.UnresolvedConflicts,
]),
env: {
GIT_EDITOR: ':',
GIT_EDITOR: gitEditor,
},
}
@ -524,3 +526,64 @@ export async function continueRebase(
return parseRebaseResult(result)
}
/**
* Method for initiating interactive rebase in the app.
*
* In order to modify the interactive todo list during interactive rebase, we
* create a temporary todo list of our own. Pass that file's path into our
* interactive rebase and using the sequence.editor to cat replace the
* interactive todo list with the contents of our generated one.
*
* @param pathOfGeneratedTodo path to generated todo list for interactive rebase
* @param lastRetainedCommitRef the commit before the earliest commit to be changed during the interactive rebase
* @param action a description of the action to be displayed in the progress dialog - i.e. Squash, Amend, etc..
*/
export async function rebaseInteractive(
repository: Repository,
pathOfGeneratedTodo: string,
lastRetainedCommitRef: string,
action: string = 'interactive rebase',
gitEditor: string = ':',
progressCallback?: (progress: IRebaseProgress) => void
): Promise<RebaseResult> {
const baseOptions: IGitExecutionOptions = {
expectedErrors: new Set([GitError.RebaseConflicts]),
env: {
GIT_EDITOR: gitEditor,
},
}
let options = baseOptions
if (progressCallback !== undefined) {
const commits = await getCommits(repository, lastRetainedCommitRef)
if (commits === null) {
log.warn(`Unable to interactively rebase if no commits in revision`)
return RebaseResult.Error
}
// TODO: pass in a rebase action for parser - Rebase, Squash, etc..
options = configureOptionsForRebase(baseOptions, {
commits,
progressCallback,
})
}
const result = await git(
[
'-c',
// This replaces interactive todo with contents of file at pathOfGeneratedTodo
`sequence.editor=cat "${pathOfGeneratedTodo}" >`,
'rebase',
'-i',
lastRetainedCommitRef,
],
repository.path,
action,
options
)
return parseRebaseResult(result)
}

106
app/src/lib/git/squash.ts Normal file
View file

@ -0,0 +1,106 @@
import * as FSE from 'fs-extra'
import { getCommits, revRange } from '.'
import { CommitOneLine } from '../../models/commit'
import { Repository } from '../../models/repository'
import { getTempFilePath } from '../file-system'
import { rebaseInteractive, RebaseResult } from './rebase'
/**
* Squashes provided commits by calling interactive rebase
*
* @param toSquash - commits to squash onto another commit
* @param squashOnto - commit to squash the `toSquash` commits onto
* @param lastRetainedCommitRef - sha of commit before commits in squash
* @param commitMessage - the first line of the string provided will be the
* summary and rest the body (similar to commit implementation)
*/
export async function squash(
repository: Repository,
toSquash: ReadonlyArray<CommitOneLine>,
squashOnto: CommitOneLine,
lastRetainedCommitRef: string,
commitMessage: string
): Promise<RebaseResult> {
let messagePath, todoPath
let result: RebaseResult
try {
if (toSquash.length === 0) {
throw new Error('[squash] No commits provided to squash.')
}
const commits = await getCommits(
repository,
revRange(lastRetainedCommitRef, 'HEAD')
)
if (commits.length === 0) {
throw new Error(
'[squash] Could not find commits in log for last retained commit ref.'
)
}
todoPath = await getTempFilePath('squashTodo')
let foundSquashOntoCommitInLog = false
// need to traverse in reverse so we do oldest to newest (replay commits)
for (let i = commits.length - 1; i >= 0; i--) {
// Ignore commits to squash because those are written right next to the target commit
if (toSquash.map(sq => sq.sha).includes(commits[i].sha)) {
continue
}
await FSE.appendFile(
todoPath,
`pick ${commits[i].sha} ${commits[i].summary}\n`
)
// If it's the target commit, write a `squash` line for every commit to squash
if (commits[i].sha === squashOnto.sha) {
foundSquashOntoCommitInLog = true
for (let j = 0; j < toSquash.length; j++) {
await FSE.appendFile(
todoPath,
`squash ${toSquash[j].sha} ${toSquash[j].summary}\n`
)
}
}
}
if (!foundSquashOntoCommitInLog) {
throw new Error(
'[squash] The commit to squash onto was not in the log. Continuing would result in dropping the commits in the toSquash array.'
)
}
if (commitMessage.trim() !== '') {
messagePath = await getTempFilePath('squashCommitMessage')
await FSE.writeFile(messagePath, commitMessage)
}
// if no commit message provided, accept default editor
const gitEditor =
messagePath !== undefined ? `cat "${messagePath}" >` : undefined
result = await rebaseInteractive(
repository,
todoPath,
lastRetainedCommitRef,
'squash',
gitEditor
// TODO: add progress
)
} catch (e) {
log.error(e)
return RebaseResult.Error
} finally {
if (todoPath !== undefined) {
FSE.remove(todoPath)
}
if (messagePath !== undefined) {
FSE.remove(messagePath)
}
}
return result
}

View file

@ -0,0 +1,318 @@
import * as FSE from 'fs-extra'
import * as Path from 'path'
import {
continueRebase,
getChangedFiles,
getCommit,
getCommits,
getRebaseInternalState,
RebaseResult,
} from '../../../src/lib/git'
import { CommitOneLine } from '../../../src/models/commit'
import { Repository } from '../../../src/models/repository'
import { setupEmptyRepositoryDefaultMain } from '../../helpers/repositories'
import { makeCommit } from '../../helpers/repository-scaffolding'
import { squash } from '../../../src/lib/git/squash'
import { GitProcess } from 'dugite'
import { getStatusOrThrow } from '../../helpers/status'
import { getTempFilePath } from '../../../src/lib/file-system'
describe('git/cherry-pick', () => {
let repository: Repository
let initialCommit: CommitOneLine
beforeEach(async () => {
repository = await setupEmptyRepositoryDefaultMain()
initialCommit = await makeSquashCommit(repository, 'initialize')
})
it('squashes one commit onto the next (non-conflicting)', async () => {
const firstCommit = await makeSquashCommit(repository, 'first')
const secondCommit = await makeSquashCommit(repository, 'second')
const result = await squash(
repository,
[secondCommit],
firstCommit,
initialCommit.sha,
'Test Summary\n\nTest Body'
)
expect(result).toBe(RebaseResult.CompletedWithoutError)
const log = await getCommits(repository, 'HEAD', 5)
const squashed = log[0]
expect(squashed.summary).toBe('Test Summary')
expect(squashed.body).toBe('Test Body\n')
expect(log.length).toBe(2)
// verify squashed commit contains changes from squashed commits
const squashedFiles = await getChangedFiles(repository, squashed.sha)
const squashedFilePaths = squashedFiles.map(f => f.path).join(' ')
expect(squashedFilePaths).toContain('first.md')
expect(squashedFilePaths).toContain('second.md')
})
it('squashes multiple commit onto one (non-conflicting)', async () => {
const firstCommit = await makeSquashCommit(repository, 'first')
const secondCommit = await makeSquashCommit(repository, 'second')
const thirdCommit = await makeSquashCommit(repository, 'third')
const fourthCommit = await makeSquashCommit(repository, 'fourth')
const result = await squash(
repository,
[secondCommit, thirdCommit, fourthCommit],
firstCommit,
initialCommit.sha,
'Test Summary\n\nTest Body'
)
expect(result).toBe(RebaseResult.CompletedWithoutError)
const log = await getCommits(repository, 'HEAD', 5)
const squashed = log[0]
expect(squashed.summary).toBe('Test Summary')
expect(squashed.body).toBe('Test Body\n')
expect(log.length).toBe(2)
// verify squashed commit contains changes from squashed commits
const squashedFiles = await getChangedFiles(repository, squashed.sha)
const squashedFilePaths = squashedFiles.map(f => f.path).join(' ')
expect(squashedFilePaths).toContain('first.md')
expect(squashedFilePaths).toContain('second.md')
expect(squashedFilePaths).toContain('third.md')
expect(squashedFilePaths).toContain('fourth.md')
})
it('squashes multiple commit non-sequential commits (reorders, non-conflicting)', async () => {
const firstCommit = await makeSquashCommit(repository, 'first')
const secondCommit = await makeSquashCommit(repository, 'second')
await makeSquashCommit(repository, 'third')
const fourthCommit = await makeSquashCommit(repository, 'fourth')
// From oldest to newest, log looks like:
// - initial commit
// - 'first' commit
// - 'second' commit
// - 'third' commit
// - 'fourth' commit
// Squashing 'second' and 'fourth' onto 'first'
// Thus, reordering 'fourth' to be before 'third'
const result = await squash(
repository,
[secondCommit, fourthCommit],
firstCommit,
initialCommit.sha,
'Test Summary\n\nTest Body'
)
expect(result).toBe(RebaseResult.CompletedWithoutError)
// From oldest to newest, log should look like:
// - initial commit - log[2]
// - the squashed commit 'Test Summary' - log[1]
// - 'third' commit - log[0]
const log = await getCommits(repository, 'HEAD', 5)
const squashed = log[1]
expect(squashed.summary).toBe('Test Summary')
expect(squashed.body).toBe('Test Body\n')
expect(log[0].summary).toBe('third')
expect(log.length).toBe(3)
// verify squashed commit contains changes from squashed commits
const squashedFiles = await getChangedFiles(repository, squashed.sha)
const squashedFilePaths = squashedFiles.map(f => f.path).join(' ')
expect(squashedFilePaths).toContain('first.md')
expect(squashedFilePaths).toContain('second.md')
expect(squashedFilePaths).toContain('fourth.md')
expect(squashedFilePaths).not.toContain('third.md')
})
it('handles squashing a conflicting commit', async () => {
const firstCommit = await makeSquashCommit(repository, 'first')
// make a commit with a commit message 'second' and adding file 'second.md'
await makeSquashCommit(repository, 'second')
// make a third commit modifying 'second.md' from secondCommit
const thirdCommit = await makeSquashCommit(repository, 'third', 'second')
// squash third commit onto first commit
// Will cause a conflict due to modifications to 'second.md' - a file that
// does not exist in the first commit.
const result = await squash(
repository,
[thirdCommit],
firstCommit,
initialCommit.sha,
'Test Summary\n\nTest Body'
)
expect(result).toBe(RebaseResult.ConflictsEncountered)
let status = await getStatusOrThrow(repository)
let { files } = status.workingDirectory
// resolve conflicts by adding the conflicting file
await GitProcess.exec(
['add', Path.join(repository.path, 'second.md')],
repository.path
)
// If there are conflicts, we need to resend in git editor for changing the
// git message on continue
const messagePath = await getTempFilePath('squashCommitMessage')
await FSE.writeFile(messagePath, 'Test Summary\n\nTest Body')
// continue rebase
let continueResult = await continueRebase(
repository,
files,
undefined,
undefined,
`cat "${messagePath}" >`
)
// This will now conflict with the 'second' commit since it is going to now
// apply the second commit which now modifies the same lines in the
// 'second.md' that the squashed first commit does.
expect(continueResult).toBe(RebaseResult.ConflictsEncountered)
status = await getStatusOrThrow(repository)
files = status.workingDirectory.files
await FSE.writeFile(
Path.join(repository.path, 'second.md'),
'# resolve conflict from adding add after resolving squash'
)
continueResult = await continueRebase(
repository,
files,
undefined,
undefined,
// Only reason I did this here is to show it does not cause harm.
// In case of multiple commits being squashed/reordered before the squash
// completes, we may not be able to tell which conflict the squash
// message will need to go after so we will be sending it on all
// continues.
`cat "${messagePath}" >`
)
expect(continueResult).toBe(RebaseResult.CompletedWithoutError)
const log = await getCommits(repository, 'HEAD', 5)
expect(log.length).toBe(3)
const squashed = log[1]
expect(squashed.summary).toBe('Test Summary')
expect(squashed.body).toBe('Test Body\n')
// verify squashed commit contains changes from squashed commits
const squashedFiles = await getChangedFiles(repository, squashed.sha)
const squashedFilePaths = squashedFiles.map(f => f.path).join(' ')
expect(squashedFilePaths).toContain('first.md')
expect(squashedFilePaths).toContain('second.md')
})
it('squashes with default merged commit message/description if commit message not provided', async () => {
const firstCommit = await makeSquashCommit(repository, 'first')
const secondCommit = await makeSquashCommit(repository, 'second')
const result = await squash(
repository,
[secondCommit],
firstCommit,
initialCommit.sha,
''
)
expect(result).toBe(RebaseResult.CompletedWithoutError)
const log = await getCommits(repository, 'HEAD', 5)
const squashed = log[0]
expect(squashed.summary).toBe('first')
expect(squashed.body).toBe('second\n')
expect(log.length).toBe(2)
})
it('returns error on invalid lastRetainedCommitRef', async () => {
const firstCommit = await makeSquashCommit(repository, 'first')
const secondCommit = await makeSquashCommit(repository, 'second')
const result = await squash(
repository,
[secondCommit],
firstCommit,
'INVALID INVALID',
'Test Summary\n\nTest Body'
)
expect(result).toBe(RebaseResult.Error)
// Rebase will not start - As it won't be able retrieve a commits to build a
// todo and then interactive rebase would fail for bad revision. Added logic
// to short circuit to prevent unnecessary attempt at an interactive rebase.
const isRebaseStillOngoing = await getRebaseInternalState(repository)
expect(isRebaseStillOngoing !== null).toBeFalse()
})
it('returns error on invalid commit to squashOnto', async () => {
await makeSquashCommit(repository, 'first')
const secondCommit = await makeSquashCommit(repository, 'second')
const result = await squash(
repository,
[secondCommit],
{ sha: 'INVALID', summary: 'INVALID' },
initialCommit.sha,
'Test Summary\n\nTest Body'
)
expect(result).toBe(RebaseResult.Error)
// Rebase should not start - if we did attempt this, it could result in
// dropping commits.
const isRebaseStillOngoing = await getRebaseInternalState(repository)
expect(isRebaseStillOngoing !== null).toBeFalse()
})
it('returns error on empty toSquash', async () => {
const first = await makeSquashCommit(repository, 'first')
await makeSquashCommit(repository, 'second')
const result = await squash(
repository,
[],
first,
initialCommit.sha,
'Test Summary\n\nTest Body'
)
expect(result).toBe(RebaseResult.Error)
// Rebase should not start - technically there would be no harm in this
// rebase as it would just replay history, but we should not use squash to
// replay history.
const isRebaseStillOngoing = await getRebaseInternalState(repository)
expect(isRebaseStillOngoing !== null).toBeFalse()
})
})
async function makeSquashCommit(
repository: Repository,
desc: string,
file?: string
): Promise<CommitOneLine> {
file = file || desc
const commitTree = {
commitMessage: desc,
entries: [
{
path: file + '.md',
contents: '# ' + desc + ' \n',
},
],
}
await makeCommit(repository, commitTree)
return (await getCommit(repository, 'HEAD'))!
}

View file

@ -55,10 +55,10 @@ ansi-styles@^3.1.0:
dependencies:
color-convert "^1.9.0"
app-path@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/app-path/-/app-path-3.2.0.tgz#06d426e0c988885264e0aa0a766dfa2723491633"
integrity sha512-PQPaKXi64FZuofJkrtaO3I5RZESm9Yjv7tkeJaDz4EZMeBBfGtr5MyQ3m5AC7F0HVrISBLatPxAAAgvbe418fQ==
app-path@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/app-path/-/app-path-3.3.0.tgz#0342a909db37079c593979c720f99e872475eba3"
integrity sha512-EAgEXkdcxH1cgEePOSsmUtw9ItPl0KTxnh/pj9ZbhvbKbij9x0oX6PWpGnorDr0DS5AosLgoa5n3T/hZmKQpYA==
dependencies:
execa "^1.0.0"
@ -311,9 +311,12 @@ delegates@^1.0.0:
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
desktop-trampoline@desktop/desktop-trampoline#v0.9.4:
version "0.9.4"
resolved "https://codeload.github.com/desktop/desktop-trampoline/tar.gz/7eabea0f1a3f8432307719a1bd16d1ffde2bf6f6"
desktop-trampoline@desktop/desktop-trampoline#v0.9.7:
version "0.9.7"
resolved "https://codeload.github.com/desktop/desktop-trampoline/tar.gz/38c590851f84e1dc856b6fd7e49920ef360c2ca0"
dependencies:
node-addon-api "^3.1.0"
prebuild-install "^6.0.0"
detect-libc@^1.0.3:
version "1.0.3"
@ -364,10 +367,10 @@ double-ended-queue@^2.1.0-0:
resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c"
integrity sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=
dugite@^1.102.0:
version "1.102.0"
resolved "https://registry.yarnpkg.com/dugite/-/dugite-1.102.0.tgz#15fde8ec6726f03dabcee06e7e5ee5492bf9bf74"
integrity sha512-z3eqQd0sCAt/6HgEDFDKw/VFp9LJG5v7MpR0wQAZh4Y/vz/pt5rdjJozVGVX9kGP3sHbIVz251Ooi8lrLupUeQ==
dugite@^1.103.0:
version "1.103.0"
resolved "https://registry.yarnpkg.com/dugite/-/dugite-1.103.0.tgz#2229c83790782116f96b87763d9ea1a0f2a55842"
integrity sha512-8rKO/jQX2HKfSd5wNG/l3HnUfQPKqyC3+D+3CR5Go4+BJOyCPScQwiAVW+eeKLqHFOvjq/w67+ymMyPGxUqhIA==
dependencies:
checksum "^0.1.1"
got "^9.6.0"
@ -536,13 +539,13 @@ focus-trap@^6.1.0:
dependencies:
tabbable "^5.1.0"
fs-admin@^0.15.0:
version "0.15.0"
resolved "https://registry.yarnpkg.com/fs-admin/-/fs-admin-0.15.0.tgz#ffa7daab2f1184ca81df73e7db8365ee1747410c"
integrity sha512-2czA7rZNnu/RSVwxZUSX4fF48PRj8gJ7fKMKpY+G3ESiEzHMPCHvNQaC02PhU+PAyzBUiqfiMHJisnj4LONwLA==
fs-admin@^0.19.0:
version "0.19.0"
resolved "https://registry.yarnpkg.com/fs-admin/-/fs-admin-0.19.0.tgz#c2b077b21607ca1982bf9bc8c3fc096be7a1186e"
integrity sha512-GtJUlSqX95Daw1zlH9PtqMIpr+yQqUnCRKxupuwdlPGy4ds+ICNT3apyQlnT1yXiXvAdnTK06ag/4jMS/jzhXQ==
dependencies:
nan "^2.13.2"
prebuild-install "5.3.5"
node-addon-api "^3.1.0"
prebuild-install "^6.0.0"
fs-constants@^1.0.0:
version "1.0.0"
@ -814,10 +817,10 @@ keyboardevents-areequal@^0.2.1:
resolved "https://registry.yarnpkg.com/keyboardevents-areequal/-/keyboardevents-areequal-0.2.2.tgz#88191ec738ce9f7591c25e9056de928b40277194"
integrity sha512-Nv+Kr33T0mEjxR500q+I6IWisOQ0lK1GGOncV0kWE6n4KFmpcu7RUX5/2B0EUtX51Cb0HjZ9VJsSY3u4cBa0kw==
keytar@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.2.0.tgz#4db2bec4f9700743ffd9eda22eebb658965c8440"
integrity sha512-ECSaWvoLKI5SI0pGpZQeUV1/lpBYfkaxvoSp3zkiPOz05VavwSfLi8DdEaa9N2ekQZv3Chy+o7aP6n9mairBgw==
keytar@^7.7.0:
version "7.7.0"
resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.7.0.tgz#3002b106c01631aa79b1aa9ee0493b94179bbbd2"
integrity sha512-YEY9HWqThQc5q5xbXbRwsZTh2PJ36OSYRjSv3NN2xf5s5dpLTjEZnC2YikR29OaVybf9nQ0dJ/80i40RS97t/A==
dependencies:
node-addon-api "^3.0.0"
prebuild-install "^6.0.0"
@ -985,11 +988,6 @@ ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
nan@^2.13.2, nan@^2.14.1:
version "2.14.1"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
napi-build-utils@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.1.tgz#1381a0f92c39d66bf19852e7873432fc2123e508"
@ -1012,6 +1010,11 @@ node-addon-api@^3.0.0:
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.0.2.tgz#04bc7b83fd845ba785bb6eae25bc857e1ef75681"
integrity sha512-+D4s2HCnxPd5PjjI0STKwncjXTUKKqm74MDMz9OPXavjsGmjkvwgLtA5yoxJUdmpj52+2u+RrXgPipahKczMKg==
node-addon-api@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.1.0.tgz#98b21931557466c6729e51cb77cd39c965f42239"
integrity sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==
node-fetch@^1.0.1:
version "1.7.3"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
@ -1144,7 +1147,7 @@ plist@^2.1.0:
xmlbuilder "8.2.2"
xmldom "0.1.x"
prebuild-install@5.3.5, prebuild-install@^5.3.5:
prebuild-install@^5.3.5:
version "5.3.5"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.3.5.tgz#e7e71e425298785ea9d22d4f958dbaccf8bb0e1b"
integrity sha512-YmMO7dph9CYKi5IR/BzjOJlRzpxGGVo1EsLSUZ0mt/Mq0HWZIHOKHHcHdT69yG54C9m6i45GpItwRHpk0Py7Uw==
@ -1371,12 +1374,12 @@ regenerator-runtime@^0.13.4:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
registry-js@^1.12.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/registry-js/-/registry-js-1.12.0.tgz#35ecfba4d3c3777ff1605e239abaa823fa32979f"
integrity sha512-5xk/L83Ph3u7JY+6tb8XrnB78iDoyCwilY4/5C1VhZYiw0jeTUkdTn77kXycWpPK8jQ22LL5DQiAlNcluh+eZw==
registry-js@^1.15.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/registry-js/-/registry-js-1.15.0.tgz#eb028503df2358291e2d107fb6dc316f688e47b1"
integrity sha512-gNLm7hV3g2M5iKAdcQwoa2Zj8fhkArbzbXyQMVrK/6OfXOCaKnRNqgBnOGOJoJw4uwW9XImJuXijLf1ZKtSR3Q==
dependencies:
nan "^2.14.1"
node-addon-api "^3.1.0"
prebuild-install "^5.3.5"
responselike@^1.0.2:

View file

@ -3,6 +3,13 @@
"2.8.1": [
"[Fixed] Disable partial change selection in split view while whitespace changes are hidden - #12129"
],
"2.8.1-beta2": [
"[Improved] Add complete support for macOS on Apple Silicon devices - #12091",
"[Improved] From now on, macOS x64 builds running on Apple Silicon devices will auto update to arm64 builds"
],
"2.8.1-beta1": [
"[New] Preliminary support for macOS on Apple Silicon devices - #9691. Thanks @dennisameling!"
],
"2.8.0": [
"[New] Expand diffs to view more context around your changes - #7014",
"[New] Create aliases for repositories you want to be displayed differently in the repository list - #7856",