mirror of
https://github.com/desktop/desktop
synced 2024-09-12 21:01:16 +00:00
Merge branch 'master' into preferences
This commit is contained in:
commit
6ff8baa33e
|
@ -36,7 +36,7 @@
|
|||
"username": "^2.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"fs-extra": "^0.30.0",
|
||||
"fs-extra": "^1.0.0",
|
||||
"react-addons-perf": "15.3.2",
|
||||
"react-addons-test-utils": "15.3.2",
|
||||
"style-loader": "^0.13.1",
|
||||
|
|
|
@ -399,7 +399,7 @@ export class AppStore {
|
|||
|
||||
private onGitStoreLoadedCommits(repository: Repository, commits: ReadonlyArray<Commit>) {
|
||||
for (const commit of commits) {
|
||||
this.gitHubUserStore._loadAndCacheUser(this.users, repository, commit.sha, commit.authorEmail)
|
||||
this.gitHubUserStore._loadAndCacheUser(this.users, repository, commit.sha, commit.author.email)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { git } from './core'
|
|||
import { Repository } from '../../models/repository'
|
||||
import { Commit } from '../../models/commit'
|
||||
import { Branch, BranchType } from '../../models/branch'
|
||||
import { CommitIdentity } from '../../models/commit-identity'
|
||||
|
||||
/** Get all the branches. */
|
||||
export async function getBranches(repository: Repository, prefix: string, type: BranchType): Promise<ReadonlyArray<Branch>> {
|
||||
|
@ -13,9 +14,7 @@ export async function getBranches(repository: Repository, prefix: string, type:
|
|||
'%(refname:short)',
|
||||
'%(upstream:short)',
|
||||
'%(objectname)', // SHA
|
||||
'%(authorname)',
|
||||
'%(authoremail)',
|
||||
'%(authordate)',
|
||||
'%(author)',
|
||||
'%(parent)', // parent SHAs
|
||||
'%(subject)',
|
||||
'%(body)',
|
||||
|
@ -35,21 +34,19 @@ export async function getBranches(repository: Repository, prefix: string, type:
|
|||
const name = pieces[0].trim()
|
||||
const upstream = pieces[1]
|
||||
const sha = pieces[2]
|
||||
const authorName = pieces[3]
|
||||
|
||||
// author email is wrapped in arrows e.g. <hubot@github.com>
|
||||
const authorEmailRaw = pieces[4]
|
||||
const authorEmail = authorEmailRaw.substring(1, authorEmailRaw.length - 1)
|
||||
const authorDateText = pieces[5]
|
||||
const authorDate = new Date(authorDateText)
|
||||
const authorIdentity = pieces[3]
|
||||
const author = CommitIdentity.parseIdentity(authorIdentity)
|
||||
|
||||
const parentSHAs = pieces[6].split(' ')
|
||||
if (!author) {
|
||||
throw new Error(`Couldn't parse author identity ${authorIdentity}`)
|
||||
}
|
||||
|
||||
const summary = pieces[7]
|
||||
const parentSHAs = pieces[4].split(' ')
|
||||
const summary = pieces[5]
|
||||
const body = pieces[6]
|
||||
|
||||
const body = pieces[8]
|
||||
|
||||
const tip = new Commit(sha, summary, body, authorName, authorEmail, authorDate, parentSHAs)
|
||||
const tip = new Commit(sha, summary, body, author, parentSHAs)
|
||||
|
||||
return new Branch(name, upstream.length > 0 ? upstream : null, tip, type)
|
||||
})
|
||||
|
|
|
@ -2,6 +2,7 @@ import { git } from './core'
|
|||
import { FileStatus, FileChange } from '../../models/status'
|
||||
import { Repository } from '../../models/repository'
|
||||
import { Commit } from '../../models/commit'
|
||||
import { CommitIdentity } from '../../models/commit-identity'
|
||||
import { mapStatus } from './status'
|
||||
|
||||
/**
|
||||
|
@ -14,13 +15,22 @@ export async function getCommits(repository: Repository, revisionRange: string,
|
|||
'%H', // SHA
|
||||
'%s', // summary
|
||||
'%b', // body
|
||||
'%an', // author name
|
||||
'%ae', // author email
|
||||
'%aI', // author date, ISO-8601
|
||||
// author identity string, matching format of GIT_AUTHOR_IDENT.
|
||||
// author name <author email> <author date>
|
||||
// author date format dependent on --date arg, should be raw
|
||||
'%an <%ae> %ad',
|
||||
'%P', // parent SHAs
|
||||
].join(`%x${delimiter}`)
|
||||
|
||||
const result = await git([ 'log', revisionRange, `--max-count=${limit}`, `--pretty=${prettyFormat}`, '-z', '--no-color', ...additionalArgs ], repository.path, 'getCommits', { successExitCodes: new Set([ 0, 128 ]) })
|
||||
const result = await git([
|
||||
'log',
|
||||
revisionRange,
|
||||
`--date=raw`,
|
||||
`--max-count=${limit}`,
|
||||
`--pretty=${prettyFormat}`,
|
||||
'-z',
|
||||
'--no-color', ...additionalArgs,
|
||||
], 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) {
|
||||
|
@ -37,12 +47,16 @@ export async function getCommits(repository: Repository, revisionRange: string,
|
|||
const sha = pieces[0]
|
||||
const summary = pieces[1]
|
||||
const body = pieces[2]
|
||||
const authorName = pieces[3]
|
||||
const authorEmail = pieces[4]
|
||||
const parsedDate = Date.parse(pieces[5])
|
||||
const authorDate = new Date(parsedDate)
|
||||
const parentSHAs = pieces[6].split(' ')
|
||||
return new Commit(sha, summary, body, authorName, authorEmail, authorDate, parentSHAs)
|
||||
const authorIdentity = pieces[3]
|
||||
const parentSHAs = pieces[4].split(' ')
|
||||
|
||||
const author = CommitIdentity.parseIdentity(authorIdentity)
|
||||
|
||||
if (!author) {
|
||||
throw new Error(`Couldn't parse author identity ${authorIdentity}`)
|
||||
}
|
||||
|
||||
return new Commit(sha, summary, body, author, parentSHAs)
|
||||
})
|
||||
|
||||
return commits
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
/**
|
||||
* A tuple of name and email for the author or commit
|
||||
* A tuple of name, email, and date for the author or commit
|
||||
* info in a commit.
|
||||
*/
|
||||
export class CommitIdentity {
|
||||
|
||||
public readonly name: string
|
||||
public readonly email: string
|
||||
public readonly date: Date
|
||||
public readonly tzOffset: number
|
||||
|
||||
/**
|
||||
* Parses a Git ident string (GIT_AUTHOR_IDENT or GIT_COMMITTER_IDENT)
|
||||
|
@ -12,24 +15,44 @@ export class CommitIdentity {
|
|||
*/
|
||||
public static parseIdentity(identity: string): CommitIdentity | null {
|
||||
// See fmt_ident in ident.c:
|
||||
// https://github.com/git/git/blob/3ef7618e616e023cf04180e30d77c9fa5310f964/ident.c#L346
|
||||
// https://github.com/git/git/blob/3ef7618e6/ident.c#L346
|
||||
//
|
||||
// Format is "NAME <EMAIL> DATE"
|
||||
// Markus Olsson <j.markus.olsson@gmail.com> 1475670580 +0200
|
||||
//
|
||||
// Note that `git var` will strip any < and > from the name and email, see:
|
||||
// https://github.com/git/git/blob/3ef7618e616e023cf04180e30d77c9fa5310f964/ident.c#L396
|
||||
const m = identity.match(/^(.*?) <(.*?)>/)
|
||||
// https://github.com/git/git/blob/3ef7618e6/ident.c#L396
|
||||
//
|
||||
// Note also that this expects a date formatted with the RAW option in git see:
|
||||
// https://github.com/git/git/blob/35f6318d4/date.c#L191
|
||||
//
|
||||
const m = identity.match(/^(.*?) <(.*?)> (\d+) (\+|-)?(\d{2})(\d{2})/)
|
||||
if (!m) { return null }
|
||||
|
||||
const name = m[1]
|
||||
const email = m[2]
|
||||
// The date is specified as seconds from the epoch,
|
||||
// Date() expects milliseconds since the epoch.
|
||||
const date = new Date(parseInt(m[3], 10) * 1000)
|
||||
|
||||
return new CommitIdentity(name, email)
|
||||
// The RAW option never uses alphanumeric timezone identifiers and in my
|
||||
// testing I've never found it to omit the leading + for a positive offset
|
||||
// but the docs for strprintf seems to suggest it might on some systems so
|
||||
// we're playing it safe.
|
||||
const tzSign = m[4] === '-' ? '-' : '+'
|
||||
const tzHH = m[5]
|
||||
const tzmm = m[6]
|
||||
|
||||
const tzMinutes = parseInt(tzHH, 10) * 60 + parseInt(tzmm, 10)
|
||||
const tzOffset = tzMinutes * (tzSign === '-' ? -1 : 1)
|
||||
|
||||
return new CommitIdentity(name, email, date, tzOffset)
|
||||
}
|
||||
|
||||
public constructor(name: string, email: string) {
|
||||
public constructor(name: string, email: string, date: Date, tzOffset?: number) {
|
||||
this.name = name
|
||||
this.email = email
|
||||
this.date = date
|
||||
this.tzOffset = tzOffset || (new Date()).getTimezoneOffset()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { CommitIdentity } from './commit-identity'
|
||||
|
||||
/** A git commit. */
|
||||
export class Commit {
|
||||
/** The commit's SHA. */
|
||||
|
@ -9,25 +11,20 @@ export class Commit {
|
|||
/** The commit message without the first line and CR. */
|
||||
public readonly body: string
|
||||
|
||||
/** The commit author's name */
|
||||
public readonly authorName: string
|
||||
|
||||
/** The commit author's email address */
|
||||
public readonly authorEmail: string
|
||||
|
||||
/** The commit timestamp (with timezone information) */
|
||||
public readonly authorDate: Date
|
||||
/**
|
||||
* Information about the author of this commit.
|
||||
* includes name, email and date.
|
||||
*/
|
||||
public readonly author: CommitIdentity
|
||||
|
||||
/** The SHAs for the parents of the commit. */
|
||||
public readonly parentSHAs: ReadonlyArray<string>
|
||||
|
||||
public constructor(sha: string, summary: string, body: string, authorName: string, authorEmail: string, authorDate: Date, parentSHAs: ReadonlyArray<string>) {
|
||||
public constructor(sha: string, summary: string, body: string, author: CommitIdentity, parentSHAs: ReadonlyArray<string>) {
|
||||
this.sha = sha
|
||||
this.summary = summary
|
||||
this.body = body
|
||||
this.authorName = authorName
|
||||
this.authorEmail = authorEmail
|
||||
this.authorDate = authorDate
|
||||
this.author = author
|
||||
this.parentSHAs = parentSHAs
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,10 @@ export class DiffLine {
|
|||
public withNoTrailingNewLine(noTrailingNewLine: boolean): DiffLine {
|
||||
return new DiffLine(this.text, this.type, this.oldLineNumber, this.newLineNumber, noTrailingNewLine)
|
||||
}
|
||||
|
||||
public isIncludeableLine() {
|
||||
return this.type === DiffLineType.Add || this.type === DiffLineType.Delete
|
||||
}
|
||||
}
|
||||
|
||||
/** details about the start and end of a diff hunk */
|
||||
|
|
|
@ -695,6 +695,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
|
||||
private onSelectionChanged = (repository: Repository | CloningRepository) => {
|
||||
this.props.dispatcher.selectRepository(repository)
|
||||
this.props.dispatcher.closeFoldout()
|
||||
|
||||
if (repository instanceof Repository) {
|
||||
this.props.dispatcher.refreshGitHubRepositoryInfo(repository)
|
||||
|
|
|
@ -68,7 +68,7 @@ export class Branches extends React.Component<IBranchesProps, IBranchesState> {
|
|||
return <BranchListItem
|
||||
name={branch.name}
|
||||
isCurrentBranch={branch.name === currentBranchName}
|
||||
lastCommitDate={commit ? commit.authorDate : null}/>
|
||||
lastCommitDate={commit ? commit.author.date : null}/>
|
||||
} else {
|
||||
return <div className='branches-list-content branches-list-label'>{item.label}</div>
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ interface IUndoCommitProps {
|
|||
/** The Undo Commit component. */
|
||||
export class UndoCommit extends React.Component<IUndoCommitProps, void> {
|
||||
public render() {
|
||||
const authorDate = this.props.commit.authorDate
|
||||
const authorDate = this.props.commit.author.date
|
||||
return (
|
||||
<div id='undo-commit'>
|
||||
<div className='commit-info'>
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
import * as React from 'react'
|
||||
import { DiffLine, DiffLineType } from '../../models/diff'
|
||||
import { selectedLineClass } from './selection/selection'
|
||||
import { Diff, DiffHunk, DiffLine, DiffLineType } from '../../models/diff'
|
||||
import { hoverCssClass, selectedLineClass } from './selection/selection'
|
||||
import { assertNever } from '../../lib/fatal-error'
|
||||
import * as classNames from 'classnames'
|
||||
|
||||
/**
|
||||
* The area in pixels either side of the right-edge of the diff gutter to
|
||||
* use to detect when a group of lines should be highlighted, instead of
|
||||
* a single line.
|
||||
*/
|
||||
const EdgeDetectionSize = 10
|
||||
|
||||
/** The props for the diff gutter. */
|
||||
interface IDiffGutterProps {
|
||||
/** The line being represented by the gutter. */
|
||||
|
@ -14,18 +21,72 @@ interface IDiffGutterProps {
|
|||
* should be rendered as selected or not.
|
||||
*/
|
||||
readonly isIncluded: boolean
|
||||
|
||||
/**
|
||||
* The line number relative to the unified diff output
|
||||
*/
|
||||
readonly index: number
|
||||
|
||||
/**
|
||||
* Indicate whether the diff should handle user interactions
|
||||
*/
|
||||
readonly readOnly: boolean
|
||||
|
||||
/**
|
||||
* The diff currently displayed in the app
|
||||
*/
|
||||
readonly diff: Diff
|
||||
|
||||
/**
|
||||
* Callback to apply hover effect to lines belonging to a given hunk
|
||||
*/
|
||||
readonly updateHunkHoverState: (hunk: DiffHunk, active: boolean) => void
|
||||
|
||||
/**
|
||||
* Callback to query whether a selection gesture is currently underway
|
||||
*
|
||||
* If this returns true, the element will attempt to update the hover state.
|
||||
* Otherwise, this will defer to the active selection gesture to update
|
||||
* the visual state of the gutter.
|
||||
*/
|
||||
readonly isSelectionEnabled: () => boolean
|
||||
|
||||
/**
|
||||
* Callback to signal when the mouse button is pressed on this element
|
||||
*/
|
||||
readonly onMouseDown: (index: number, isHunkSelection: boolean) => void
|
||||
|
||||
/**
|
||||
* Callback to signal when the mouse is hovering over this element
|
||||
*/
|
||||
readonly onMouseMove: (index: number) => void
|
||||
|
||||
/**
|
||||
* Callback to signal when the mouse button is released on this element
|
||||
*/
|
||||
readonly onMouseUp: (index: number) => void
|
||||
}
|
||||
|
||||
// TODO: this doesn't consider mouse events outside the right edge
|
||||
|
||||
function isMouseInHunkSelectionZone(ev: MouseEvent): boolean {
|
||||
// MouseEvent is not generic, but getBoundingClientRect should be
|
||||
// available for all HTML elements
|
||||
// docs: https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
|
||||
|
||||
const element: any = ev.currentTarget
|
||||
const offset: ClientRect = element.getBoundingClientRect()
|
||||
const relativeLeft = ev.clientX - offset.left
|
||||
|
||||
const edge = offset.width - EdgeDetectionSize
|
||||
|
||||
return relativeLeft >= edge
|
||||
}
|
||||
|
||||
/** The gutter for a diff's line. */
|
||||
export class DiffLineGutter extends React.Component<IDiffGutterProps, void> {
|
||||
/** Can this line be selected for inclusion/exclusion? */
|
||||
private isIncludableLine(): boolean {
|
||||
return this.props.line.type === DiffLineType.Add || this.props.line.type === DiffLineType.Delete
|
||||
}
|
||||
|
||||
private isIncluded(): boolean {
|
||||
return this.isIncludableLine() && this.props.isIncluded
|
||||
}
|
||||
private elem_?: HTMLSpanElement
|
||||
|
||||
private getLineClassName(): string {
|
||||
const type = this.props.line.type
|
||||
|
@ -46,9 +107,140 @@ export class DiffLineGutter extends React.Component<IDiffGutterProps, void> {
|
|||
return classNames('diff-line-gutter', lineClass, selectedClass)
|
||||
}
|
||||
|
||||
private mouseEnterHandler = (ev: MouseEvent) => {
|
||||
ev.preventDefault()
|
||||
|
||||
const isHunkSelection = isMouseInHunkSelectionZone(ev)
|
||||
this.updateHoverState(isHunkSelection, true)
|
||||
}
|
||||
|
||||
private mouseLeaveHandler = (ev: MouseEvent) => {
|
||||
ev.preventDefault()
|
||||
|
||||
const isHunkSelection = isMouseInHunkSelectionZone(ev)
|
||||
this.updateHoverState(isHunkSelection, false)
|
||||
}
|
||||
|
||||
private updateHoverState(isHunkSelection: boolean, isActive: boolean) {
|
||||
if (isHunkSelection) {
|
||||
const hunk = this.props.diff.diffHunkForIndex(this.props.index)
|
||||
if (!hunk) {
|
||||
console.error('unable to find hunk for given line in diff')
|
||||
return
|
||||
}
|
||||
this.props.updateHunkHoverState(hunk, isActive)
|
||||
} else {
|
||||
this.setHover(isActive)
|
||||
}
|
||||
}
|
||||
|
||||
private mouseMoveHandler = (ev: MouseEvent) => {
|
||||
ev.preventDefault()
|
||||
|
||||
const hunk = this.props.diff.diffHunkForIndex(this.props.index)
|
||||
if (!hunk) {
|
||||
console.error('unable to find hunk for given line in diff')
|
||||
return
|
||||
}
|
||||
|
||||
const isHunkSelection = isMouseInHunkSelectionZone(ev)
|
||||
const isSelectionActive = this.props.isSelectionEnabled()
|
||||
|
||||
// selection is not active, perform highlighting based on mouse position
|
||||
if (isHunkSelection && isSelectionActive) {
|
||||
this.props.updateHunkHoverState(hunk, true)
|
||||
} else {
|
||||
// clear hunk selection in case hunk was previously higlighted
|
||||
this.props.updateHunkHoverState(hunk, false)
|
||||
this.setHover(true)
|
||||
}
|
||||
|
||||
this.props.onMouseMove(this.props.index)
|
||||
}
|
||||
|
||||
private mouseUpHandler = (ev: MouseEvent) => {
|
||||
ev.preventDefault()
|
||||
|
||||
this.props.onMouseUp(this.props.index)
|
||||
}
|
||||
|
||||
private mouseDownHandler = (ev: MouseEvent) => {
|
||||
ev.preventDefault()
|
||||
|
||||
const isHunkSelection = isMouseInHunkSelectionZone(ev)
|
||||
this.props.onMouseDown(this.props.index, isHunkSelection)
|
||||
}
|
||||
|
||||
private applyEventHandlers = (elem: HTMLSpanElement) => {
|
||||
// read-only diffs do not support any interactivity
|
||||
if (this.props.readOnly) {
|
||||
return
|
||||
}
|
||||
|
||||
// ignore anything from diff context rows
|
||||
if (!this.props.line.isIncludeableLine()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (elem) {
|
||||
this.elem_ = elem
|
||||
|
||||
elem.addEventListener('mouseenter', this.mouseEnterHandler)
|
||||
elem.addEventListener('mouseleave', this.mouseLeaveHandler)
|
||||
elem.addEventListener('mousemove', this.mouseMoveHandler)
|
||||
elem.addEventListener('mousedown', this.mouseDownHandler)
|
||||
elem.addEventListener('mouseup', this.mouseUpHandler)
|
||||
} else {
|
||||
|
||||
// this callback fires a second time when the DOM element
|
||||
// is unmounted, so we can use this as a chance to cleanup
|
||||
|
||||
if (this.elem_) {
|
||||
this.elem_.removeEventListener('mouseenter', this.mouseEnterHandler)
|
||||
this.elem_.removeEventListener('mouseleave', this.mouseLeaveHandler)
|
||||
this.elem_.removeEventListener('mousemove', this.mouseMoveHandler)
|
||||
this.elem_.removeEventListener('mousedown', this.mouseDownHandler)
|
||||
this.elem_.removeEventListener('mouseup', this.mouseUpHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public isIncluded(): boolean {
|
||||
return this.props.line.isIncludeableLine() && this.props.isIncluded
|
||||
}
|
||||
|
||||
public setHover(visible: boolean) {
|
||||
if (visible) {
|
||||
this.setClass(hoverCssClass)
|
||||
} else {
|
||||
this.unsetClass(hoverCssClass)
|
||||
}
|
||||
}
|
||||
|
||||
public setSelected(visible: boolean) {
|
||||
if (visible) {
|
||||
this.setClass(selectedLineClass)
|
||||
} else {
|
||||
this.unsetClass(selectedLineClass)
|
||||
}
|
||||
}
|
||||
|
||||
private setClass(cssClass: string) {
|
||||
if (this.elem_) {
|
||||
this.elem_.classList.add(cssClass)
|
||||
}
|
||||
}
|
||||
|
||||
private unsetClass(cssClass: string) {
|
||||
if (this.elem_) {
|
||||
this.elem_.classList.remove(cssClass)
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<span className={this.getLineClass()}>
|
||||
<span className={this.getLineClass()}
|
||||
ref={this.applyEventHandlers}>
|
||||
<span className='diff-line-number before'>{this.props.line.oldLineNumber || ' '}</span>
|
||||
<span className='diff-line-number after'>{this.props.line.newLineNumber || ' '}</span>
|
||||
</span>
|
||||
|
|
|
@ -12,7 +12,7 @@ import { CodeMirrorHost } from './code-mirror-host'
|
|||
import { Repository } from '../../models/repository'
|
||||
|
||||
import { FileChange, WorkingDirectoryFileChange, FileStatus } from '../../models/status'
|
||||
import { DiffHunk, DiffLine, DiffLineType, Diff as DiffModel, DiffSelection, ImageDiff } from '../../models/diff'
|
||||
import { DiffHunk, Diff as DiffModel, DiffSelection, ImageDiff } from '../../models/diff'
|
||||
import { Dispatcher } from '../../lib/dispatcher/dispatcher'
|
||||
|
||||
import { DiffLineGutter } from './diff-line-gutter'
|
||||
|
@ -21,7 +21,6 @@ import { getDiffMode } from './diff-mode'
|
|||
import { ISelectionStrategy } from './selection/selection-strategy'
|
||||
import { DragDropSelection } from './selection/drag-drop-selection-strategy'
|
||||
import { HunkSelection } from './selection/hunk-selection-strategy'
|
||||
import { hoverCssClass, selectedLineClass } from './selection/selection'
|
||||
|
||||
import { fatalError } from '../../lib/fatal-error'
|
||||
|
||||
|
@ -30,6 +29,21 @@ if (__DARWIN__) {
|
|||
require('codemirror/addon/scroll/simplescrollbars')
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* normalize the line endings in the diff so that the CodeMirror editor
|
||||
* will display the unified diff correctly
|
||||
*/
|
||||
function formatLineEnding(text: string): string {
|
||||
if (text.endsWith('\n')) {
|
||||
return text
|
||||
} else if (text.endsWith('\r')) {
|
||||
return text + '\n'
|
||||
} else {
|
||||
return text + '\r\n'
|
||||
}
|
||||
}
|
||||
|
||||
/** The props for the Diff component. */
|
||||
interface IDiffProps {
|
||||
readonly repository: Repository
|
||||
|
@ -80,8 +94,7 @@ export class Diff extends React.Component<IDiffProps, void> {
|
|||
/**
|
||||
* a local cache of gutter elements, keyed by the row in the diff
|
||||
*/
|
||||
private cachedGutterElements = new Map<number, HTMLSpanElement>()
|
||||
|
||||
private cachedGutterElements = new Map<number, DiffLineGutter>()
|
||||
|
||||
public componentWillReceiveProps(nextProps: IDiffProps) {
|
||||
// If we're reloading the same file, we want to save the current scroll
|
||||
|
@ -114,22 +127,15 @@ export class Diff extends React.Component<IDiffProps, void> {
|
|||
|
||||
const diff = nextProps.diff
|
||||
this.cachedGutterElements.forEach((element, index) => {
|
||||
const childSpan = element.children[0] as HTMLSpanElement
|
||||
if (!childSpan) {
|
||||
if (!element) {
|
||||
console.error('expected DOM element for diff gutter not found')
|
||||
return
|
||||
}
|
||||
|
||||
const line = diff.diffLineForIndex(index)
|
||||
const isIncludable = line
|
||||
? line.type === DiffLineType.Add || line.type === DiffLineType.Delete
|
||||
: false
|
||||
|
||||
if (selection.isSelected(index) && isIncludable) {
|
||||
childSpan.classList.add(selectedLineClass)
|
||||
} else {
|
||||
childSpan.classList.remove(selectedLineClass)
|
||||
}
|
||||
const isIncludable = line ? line.isIncludeableLine() : false
|
||||
const isSelected = selection.isSelected(index) && isIncludable
|
||||
element.setSelected(isSelected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -141,20 +147,55 @@ export class Diff extends React.Component<IDiffProps, void> {
|
|||
private dispose() {
|
||||
this.codeMirror = null
|
||||
|
||||
this.lineCleanup.forEach((disposable) => disposable.dispose())
|
||||
this.lineCleanup.forEach(disposable => disposable.dispose())
|
||||
this.lineCleanup.clear()
|
||||
}
|
||||
|
||||
private onMouseDown(index: number, selected: boolean, isHunkSelection: boolean) {
|
||||
if (this.props.readOnly) {
|
||||
private updateHunkHoverState = (hunk: DiffHunk, show: boolean) => {
|
||||
const start = hunk.unifiedDiffStart
|
||||
|
||||
hunk.lines.forEach((line, index) => {
|
||||
if (line.isIncludeableLine()) {
|
||||
const row = start + index
|
||||
this.highlightLine(row, show)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private highlightLine = (row: number, include: boolean) => {
|
||||
const element = this.cachedGutterElements.get(row)
|
||||
|
||||
if (!element) {
|
||||
// element not currently cached by the editor, don't try and update it
|
||||
return
|
||||
}
|
||||
|
||||
element.setHover(include)
|
||||
}
|
||||
|
||||
private onMouseUp = () => {
|
||||
if (!this.props.onIncludeChanged) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.selection) {
|
||||
return
|
||||
}
|
||||
|
||||
this.props.onIncludeChanged(this.selection.done())
|
||||
|
||||
// operation is completed, clean this up
|
||||
this.selection = null
|
||||
}
|
||||
|
||||
private onMouseDown = (index: number, isHunkSelection: boolean) => {
|
||||
if (!(this.props.file instanceof WorkingDirectoryFileChange)) {
|
||||
fatalError('must not start selection when selected file is not a WorkingDirectoryFileChange')
|
||||
return
|
||||
}
|
||||
|
||||
const snapshot = this.props.file.selection
|
||||
const selected = snapshot.isSelected(index)
|
||||
const desiredSelection = !selected
|
||||
|
||||
if (isHunkSelection) {
|
||||
|
@ -174,77 +215,12 @@ export class Diff extends React.Component<IDiffProps, void> {
|
|||
this.selection.paint(this.cachedGutterElements)
|
||||
}
|
||||
|
||||
private onMouseUp(index: number) {
|
||||
if (this.props.readOnly || !this.props.onIncludeChanged) {
|
||||
return
|
||||
private onMouseMove = (index: number) => {
|
||||
if (this.selection) {
|
||||
this.selection.update(index)
|
||||
this.selection.paint(this.cachedGutterElements)
|
||||
}
|
||||
|
||||
if (!(this.props.file instanceof WorkingDirectoryFileChange)) {
|
||||
fatalError('must not complete selection when selected file is not a WorkingDirectoryFileChange')
|
||||
return
|
||||
}
|
||||
|
||||
const selection = this.selection
|
||||
if (!selection) {
|
||||
return
|
||||
}
|
||||
|
||||
selection.apply(this.props.onIncludeChanged)
|
||||
|
||||
// operation is completed, clean this up
|
||||
this.selection = null
|
||||
}
|
||||
|
||||
private isIncludableLine(line: DiffLine): boolean {
|
||||
return line.type === DiffLineType.Add || line.type === DiffLineType.Delete
|
||||
}
|
||||
|
||||
private isMouseInHunkSelectionZone(ev: MouseEvent): boolean {
|
||||
// MouseEvent is not generic, but getBoundingClientRect should be
|
||||
// available for all HTML elements
|
||||
// docs: https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
|
||||
|
||||
const element: any = ev.currentTarget
|
||||
const offset: ClientRect = element.getBoundingClientRect()
|
||||
const relativeLeft = ev.clientX - offset.left
|
||||
|
||||
const edge = offset.width - 10
|
||||
|
||||
return relativeLeft >= edge
|
||||
}
|
||||
|
||||
private highlightHunk(hunk: DiffHunk, show: boolean) {
|
||||
const start = hunk.unifiedDiffStart
|
||||
|
||||
hunk.lines.forEach((line, index) => {
|
||||
if (this.isIncludableLine(line)) {
|
||||
const row = start + index
|
||||
this.highlightLine(row, show)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private highlightLine(row: number, include: boolean) {
|
||||
const element = this.cachedGutterElements.get(row)
|
||||
|
||||
if (!element) {
|
||||
// no point trying to render this element, as it's not
|
||||
// currently cached by the editor
|
||||
return
|
||||
}
|
||||
|
||||
const childSpan = element.children[0] as HTMLSpanElement
|
||||
if (!childSpan) {
|
||||
console.error('expected DOM element for diff gutter not found')
|
||||
return
|
||||
}
|
||||
|
||||
if (include) {
|
||||
childSpan.classList.add(hoverCssClass)
|
||||
} else {
|
||||
childSpan.classList.remove(hoverCssClass)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public renderLine = (instance: any, line: any, element: HTMLElement) => {
|
||||
|
||||
|
@ -258,7 +234,7 @@ export class Diff extends React.Component<IDiffProps, void> {
|
|||
this.lineCleanup.delete(line)
|
||||
}
|
||||
|
||||
const index = instance.getLineNumber(line)
|
||||
const index = instance.getLineNumber(line) as number
|
||||
const hunk = this.props.diff.diffHunkForIndex(index)
|
||||
if (hunk) {
|
||||
const relativeIndex = index - hunk.unifiedDiffStart
|
||||
|
@ -268,99 +244,33 @@ export class Diff extends React.Component<IDiffProps, void> {
|
|||
|
||||
const reactContainer = document.createElement('span')
|
||||
|
||||
const mouseEnterHandler = (ev: MouseEvent) => {
|
||||
ev.preventDefault()
|
||||
|
||||
if (!this.isIncludableLine(diffLine)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.isMouseInHunkSelectionZone(ev)) {
|
||||
this.highlightHunk(hunk, true)
|
||||
} else {
|
||||
this.highlightLine(index, true)
|
||||
}
|
||||
}
|
||||
|
||||
const mouseLeaveHandler = (ev: MouseEvent) => {
|
||||
ev.preventDefault()
|
||||
|
||||
if (!this.isIncludableLine(diffLine)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.isMouseInHunkSelectionZone(ev)) {
|
||||
this.highlightHunk(hunk, false)
|
||||
} else {
|
||||
this.highlightLine(index, false)
|
||||
}
|
||||
}
|
||||
|
||||
const mouseDownHandler = (ev: MouseEvent) => {
|
||||
ev.preventDefault()
|
||||
|
||||
const isHunkSelection = this.isMouseInHunkSelectionZone(ev)
|
||||
|
||||
let isIncluded = false
|
||||
if (this.props.file instanceof WorkingDirectoryFileChange) {
|
||||
isIncluded = this.props.file.selection.isSelected(index)
|
||||
}
|
||||
this.onMouseDown(index, isIncluded, isHunkSelection)
|
||||
}
|
||||
|
||||
const mouseMoveHandler = (ev: MouseEvent) => {
|
||||
|
||||
ev.preventDefault()
|
||||
|
||||
// ignoring anything from diff context rows
|
||||
if (!this.isIncludableLine(diffLine)) {
|
||||
return
|
||||
}
|
||||
|
||||
// if selection is active, perform highlighting
|
||||
if (!this.selection) {
|
||||
|
||||
// clear hunk selection in case transitioning from hunk->line
|
||||
this.highlightHunk(hunk, false)
|
||||
|
||||
if (this.isMouseInHunkSelectionZone(ev)) {
|
||||
this.highlightHunk(hunk, true)
|
||||
} else {
|
||||
this.highlightLine(index, true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
this.selection.update(index)
|
||||
this.selection.paint(this.cachedGutterElements)
|
||||
}
|
||||
|
||||
const mouseUpHandler = (ev: UIEvent) => {
|
||||
ev.preventDefault()
|
||||
|
||||
this.onMouseUp(index)
|
||||
}
|
||||
|
||||
if (!this.props.readOnly) {
|
||||
reactContainer.addEventListener('mouseenter', mouseEnterHandler)
|
||||
reactContainer.addEventListener('mouseleave', mouseLeaveHandler)
|
||||
reactContainer.addEventListener('mousemove', mouseMoveHandler)
|
||||
reactContainer.addEventListener('mousedown', mouseDownHandler)
|
||||
reactContainer.addEventListener('mouseup', mouseUpHandler)
|
||||
}
|
||||
|
||||
this.cachedGutterElements.set(index, reactContainer)
|
||||
|
||||
let isIncluded = false
|
||||
if (this.props.file instanceof WorkingDirectoryFileChange) {
|
||||
isIncluded = this.props.file.selection.isSelected(index)
|
||||
}
|
||||
|
||||
const cache = this.cachedGutterElements
|
||||
|
||||
ReactDOM.render(
|
||||
<DiffLineGutter
|
||||
line={diffLine}
|
||||
isIncluded={isIncluded}
|
||||
/>, reactContainer)
|
||||
index={index}
|
||||
readOnly={this.props.readOnly}
|
||||
diff={this.props.diff}
|
||||
updateHunkHoverState={this.updateHunkHoverState}
|
||||
isSelectionEnabled={this.isSelectionEnabled}
|
||||
onMouseUp={this.onMouseUp}
|
||||
onMouseDown={this.onMouseDown}
|
||||
onMouseMove={this.onMouseMove} />,
|
||||
reactContainer,
|
||||
function (this: DiffLineGutter) {
|
||||
if (this !== undefined) {
|
||||
cache.set(index, this)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
element.insertBefore(reactContainer, diffLineElement)
|
||||
|
||||
// Hack(ish?). In order to be a real good citizen we need to unsubscribe from
|
||||
|
@ -378,17 +288,8 @@ export class Diff extends React.Component<IDiffProps, void> {
|
|||
//
|
||||
// See https://facebook.github.io/react/blog/2015/10/01/react-render-and-top-level-api.html
|
||||
const gutterCleanup = new Disposable(() => {
|
||||
|
||||
this.cachedGutterElements.delete(index)
|
||||
|
||||
if (!this.props.readOnly) {
|
||||
reactContainer.removeEventListener('mouseenter', mouseEnterHandler)
|
||||
reactContainer.removeEventListener('mouseleave', mouseLeaveHandler)
|
||||
reactContainer.removeEventListener('mousedown', mouseDownHandler)
|
||||
reactContainer.removeEventListener('mousemove', mouseMoveHandler)
|
||||
reactContainer.removeEventListener('mouseup', mouseUpHandler)
|
||||
}
|
||||
|
||||
ReactDOM.unmountComponentAtNode(reactContainer)
|
||||
|
||||
line.off('delete', deleteHandler)
|
||||
|
@ -446,20 +347,6 @@ export class Diff extends React.Component<IDiffProps, void> {
|
|||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* normalize the line endings in the diff so that the CodeMirror Editor
|
||||
* will display the unified diff correctly
|
||||
*/
|
||||
private formatLineEnding(text: string): string {
|
||||
if (text.endsWith('\n')) {
|
||||
return text
|
||||
} else if (text.endsWith('\r')) {
|
||||
return text + '\n'
|
||||
} else {
|
||||
return text + '\r\n'
|
||||
}
|
||||
}
|
||||
|
||||
private getAndStoreCodeMirrorInstance = (cmh: CodeMirrorHost) => {
|
||||
this.codeMirror = cmh === null ? null : cmh.getEditor()
|
||||
}
|
||||
|
@ -479,7 +366,7 @@ export class Diff extends React.Component<IDiffProps, void> {
|
|||
let diffText = ''
|
||||
|
||||
this.props.diff.hunks.forEach(hunk => {
|
||||
hunk.lines.forEach(l => diffText += this.formatLineEnding(l.text))
|
||||
hunk.lines.forEach(l => diffText += formatLineEnding(l.text))
|
||||
})
|
||||
|
||||
const options: IEditorConfigurationExtra = {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { DiffSelection } from '../../../models/diff'
|
||||
import { ISelectionStrategy } from './selection-strategy'
|
||||
import { selectedLineClass } from './selection'
|
||||
import { DiffLineGutter } from '../diff-line-gutter'
|
||||
import { compare } from '../../../lib/compare'
|
||||
import { range } from '../../../lib/range'
|
||||
|
||||
|
@ -68,7 +68,7 @@ export class DragDropSelection implements ISelectionStrategy {
|
|||
/**
|
||||
* apply the selection strategy result to the current diff
|
||||
*/
|
||||
public apply(onIncludeChanged: (diffSelection: DiffSelection) => void) {
|
||||
public done(): DiffSelection {
|
||||
const length = (this.upperIndex - this.lowerIndex) + 1
|
||||
|
||||
const newSelection = this.snapshot.withRangeSelection(
|
||||
|
@ -76,14 +76,14 @@ export class DragDropSelection implements ISelectionStrategy {
|
|||
length,
|
||||
this.desiredSelection)
|
||||
|
||||
onIncludeChanged(newSelection)
|
||||
return newSelection
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the range of lines to repaint, based on how far the user
|
||||
* has moved their cursor
|
||||
*/
|
||||
private determineDirtyRange(elements: Map<number, HTMLSpanElement>): { start: number, end: number} {
|
||||
private determineDirtyRange(elements: Map<number, DiffLineGutter>): { start: number, end: number} {
|
||||
|
||||
// as user can go back and forth when doing drag-and-drop, we should
|
||||
// update rows outside the current selected range
|
||||
|
@ -123,7 +123,7 @@ export class DragDropSelection implements ISelectionStrategy {
|
|||
/**
|
||||
* repaint the current diff gutter to visualize the current state
|
||||
*/
|
||||
public paint(elements: Map<number, HTMLSpanElement>) {
|
||||
public paint(elements: Map<number, DiffLineGutter>) {
|
||||
|
||||
const { start, end } = this.determineDirtyRange(elements)
|
||||
|
||||
|
@ -135,17 +135,7 @@ export class DragDropSelection implements ISelectionStrategy {
|
|||
}
|
||||
|
||||
const selected = this.getIsSelected(row)
|
||||
const childSpan = element.children[0] as HTMLSpanElement
|
||||
if (!childSpan) {
|
||||
console.error('expected DOM element for diff gutter not found')
|
||||
return
|
||||
}
|
||||
|
||||
if (selected) {
|
||||
childSpan.classList.add(selectedLineClass)
|
||||
} else {
|
||||
childSpan.classList.remove(selectedLineClass)
|
||||
}
|
||||
element.setSelected(selected)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { DiffSelection } from '../../../models/diff'
|
||||
import { ISelectionStrategy } from './selection-strategy'
|
||||
import { DiffLineGutter } from '../diff-line-gutter'
|
||||
import { range } from '../../../lib/range'
|
||||
import { selectedLineClass } from './selection'
|
||||
|
||||
/** apply hunk selection to the current diff */
|
||||
export class HunkSelection implements ISelectionStrategy {
|
||||
|
@ -21,7 +21,7 @@ export class HunkSelection implements ISelectionStrategy {
|
|||
// no-op
|
||||
}
|
||||
|
||||
public paint(elements: Map<number, HTMLSpanElement>) {
|
||||
public paint(elements: Map<number, DiffLineGutter>) {
|
||||
range(this._start, this._end).forEach(row => {
|
||||
const element = elements.get(row)
|
||||
|
||||
|
@ -30,28 +30,17 @@ export class HunkSelection implements ISelectionStrategy {
|
|||
return
|
||||
}
|
||||
|
||||
// HACK: Don't update classes for non-selectable lines
|
||||
const classList = element.children[0].classList
|
||||
if (!classList.contains('diff-add') && !classList.contains('diff-delete')) {
|
||||
if (!element.isIncluded()) {
|
||||
return
|
||||
}
|
||||
|
||||
const selected = this._desiredSelection
|
||||
const childSpan = element.children[0] as HTMLSpanElement
|
||||
if (!childSpan) {
|
||||
console.error('expected DOM element for diff gutter not found')
|
||||
return
|
||||
}
|
||||
|
||||
if (selected) {
|
||||
childSpan.classList.add(selectedLineClass)
|
||||
} else {
|
||||
childSpan.classList.remove(selectedLineClass)
|
||||
}
|
||||
element.setSelected(selected)
|
||||
})
|
||||
}
|
||||
|
||||
public apply(onIncludeChanged: (diffSelection: DiffSelection) => void) {
|
||||
public done(): DiffSelection {
|
||||
|
||||
const length = (this._end - this._start) + 1
|
||||
|
||||
|
@ -60,6 +49,6 @@ export class HunkSelection implements ISelectionStrategy {
|
|||
length,
|
||||
this._desiredSelection)
|
||||
|
||||
onIncludeChanged(newSelection)
|
||||
return newSelection
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { DiffSelection } from '../../../models/diff'
|
||||
import { DiffLineGutter } from '../diff-line-gutter'
|
||||
|
||||
export interface ISelectionStrategy {
|
||||
/**
|
||||
|
@ -8,9 +9,9 @@ export interface ISelectionStrategy {
|
|||
/**
|
||||
* repaint the current diff gutter to visualize the current state
|
||||
*/
|
||||
paint: (elements: Map<number, HTMLSpanElement>) => void
|
||||
paint: (elements: Map<number, DiffLineGutter>) => void
|
||||
/**
|
||||
* apply the selection strategy result to the current diff
|
||||
* get the diff selection now that the gesture is complete
|
||||
*/
|
||||
apply: (onIncludeChanged: (diffSelection: DiffSelection) => void) => void
|
||||
done: () => DiffSelection
|
||||
}
|
||||
|
|
|
@ -14,15 +14,15 @@ interface ICommitProps {
|
|||
/** A component which displays a single commit in a commit list. */
|
||||
export class CommitListItem extends React.Component<ICommitProps, void> {
|
||||
public render() {
|
||||
const authorDate = this.props.commit.authorDate
|
||||
const authorDate = this.props.commit.author.date
|
||||
const avatarURL = this.props.avatarURL || DefaultAvatarURL
|
||||
return (
|
||||
<div className='commit'>
|
||||
<img className='avatar' src={avatarURL}/>
|
||||
<div className='info'>
|
||||
<EmojiText className='summary' emoji={this.props.emoji}>{this.props.commit.summary}</EmojiText>
|
||||
<div className='byline' title={this.props.commit.authorDate.toString()}>
|
||||
<RelativeTime date={authorDate} /> by {this.props.commit.authorName}
|
||||
<div className='byline'>
|
||||
<RelativeTime date={authorDate} /> by {this.props.commit.author.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -25,7 +25,7 @@ export class CommitList extends React.Component<ICommitListProps, void> {
|
|||
const sha = this.props.history[row]
|
||||
const commit = this.props.commits.get(sha)
|
||||
if (commit) {
|
||||
const gitHubUser = this.props.gitHubUsers.get(commit.authorEmail.toLowerCase()) || null
|
||||
const gitHubUser = this.props.gitHubUsers.get(commit.author.email.toLowerCase()) || null
|
||||
const avatarURL = gitHubUser ? gitHubUser.avatarURL : null
|
||||
return <CommitListItem key={commit.sha} commit={commit} avatarURL={avatarURL} emoji={this.props.emoji}/>
|
||||
} else {
|
||||
|
|
|
@ -62,7 +62,7 @@ export class History extends React.Component<IHistoryProps, void> {
|
|||
summary={commit.summary}
|
||||
body={commit.body}
|
||||
sha={commit.sha}
|
||||
authorName={commit.authorName}
|
||||
authorName={commit.author.name}
|
||||
files={this.props.history.changedFiles}
|
||||
emoji={this.props.emoji}
|
||||
/>
|
||||
|
|
|
@ -53,10 +53,17 @@
|
|||
line-height: normal;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
|
||||
.before {
|
||||
border-right: 1px solid var(--diff-border-color);
|
||||
}
|
||||
|
||||
.after {
|
||||
border-right: 4px solid var(--diff-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.diff-line-number {
|
||||
border-right: 1px solid var(--diff-border-color);
|
||||
display: inline-block;
|
||||
color: var(--diff-line-number-color);
|
||||
width: var(--diff-line-number-column-width);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as path from 'path'
|
||||
|
||||
const fs = require('fs-extra')
|
||||
import * as fs from 'fs-extra'
|
||||
const temp = require('temp').track()
|
||||
|
||||
import { Repository } from '../src/models/repository'
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as chai from 'chai'
|
||||
const expect = chai.expect
|
||||
import { expect, use as chaiUse } from 'chai'
|
||||
|
||||
chaiUse(require('chai-datetime'))
|
||||
|
||||
import { CommitIdentity } from '../../src/models/commit-identity'
|
||||
|
||||
|
@ -11,6 +12,15 @@ describe('CommitIdentity', () => {
|
|||
|
||||
expect(identity!.name).to.equal('Markus Olsson')
|
||||
expect(identity!.email).to.equal('markus@github.com')
|
||||
expect(identity!.date).to.equalTime(new Date('2016-10-05T12:29:40.000Z'))
|
||||
})
|
||||
|
||||
it('parses timezone information', () => {
|
||||
const identity1 = CommitIdentity.parseIdentity('Markus Olsson <markus@github.com> 1475670580 +0130')
|
||||
expect(identity1!.tzOffset).to.equal(90)
|
||||
|
||||
const identity2 = CommitIdentity.parseIdentity('Markus Olsson <markus@github.com> 1475670580 -0245')
|
||||
expect(identity2!.tzOffset).to.equal(-165)
|
||||
})
|
||||
|
||||
it('parses even if the email address isn\'t a normal email', () => {
|
||||
|
@ -20,5 +30,14 @@ describe('CommitIdentity', () => {
|
|||
expect(identity!.name).to.equal('Markus Olsson')
|
||||
expect(identity!.email).to.equal('Markus Olsson')
|
||||
})
|
||||
|
||||
it('parses even if the email address is broken', () => {
|
||||
// https://github.com/git/git/blob/3ef7618e616e023cf04180e30d77c9fa5310f964/ident.c#L292-L296
|
||||
const identity = CommitIdentity.parseIdentity('Markus Olsson <Markus >Olsson> 1475670580 +0200')
|
||||
expect(identity).not.to.be.null
|
||||
|
||||
expect(identity!.name).to.equal('Markus Olsson')
|
||||
expect(identity!.email).to.equal('Markus >Olsson')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -15,7 +15,7 @@ import { GitProcess } from 'git-kitchen-sink'
|
|||
import { FileStatus, WorkingDirectoryFileChange } from '../../../src/models/status'
|
||||
import { DiffSelectionType, DiffSelection } from '../../../src/models/diff'
|
||||
|
||||
const fs = require('fs-extra')
|
||||
import * as fs from 'fs-extra'
|
||||
const temp = require('temp').track()
|
||||
|
||||
describe('git/commit', () => {
|
||||
|
|
|
@ -60,10 +60,9 @@ describe('git/for-each-ref', () => {
|
|||
expect(currentBranch!.tip.sha).to.equal('dfa96676b65e1c0ed43ca25492252a5e384c8efd')
|
||||
expect(currentBranch!.tip.summary).to.equal('this is a commit title')
|
||||
expect(currentBranch!.tip.body).to.contain('lucky last')
|
||||
expect(currentBranch!.tip.authorName).to.equal('Brendan Forster')
|
||||
expect(currentBranch!.tip.authorEmail).to.equal('brendan@github.com')
|
||||
const date = currentBranch!.tip.authorDate
|
||||
expect(date).to.equalDate(new Date('Tue Oct 18 16:23:42 2016 +1100'))
|
||||
expect(currentBranch!.tip.author.name).to.equal('Brendan Forster')
|
||||
expect(currentBranch!.tip.author.email).to.equal('brendan@github.com')
|
||||
expect(currentBranch!.tip.author.date).to.equalDate(new Date('Tue Oct 18 16:23:42 2016 +1100'))
|
||||
|
||||
expect(currentBranch!.tip.parentSHAs.length).to.equal(1)
|
||||
})
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as chai from 'chai'
|
|||
const expect = chai.expect
|
||||
|
||||
import * as path from 'path'
|
||||
const fs = require('fs-extra')
|
||||
import * as fs from 'fs-extra'
|
||||
|
||||
import { Repository } from '../../../src/models/repository'
|
||||
import { FileStatus, WorkingDirectoryFileChange } from '../../../src/models/status'
|
||||
|
|
|
@ -7,7 +7,7 @@ import { setupFixtureRepository, setupEmptyRepository } from '../../fixture-help
|
|||
import { FileStatus } from '../../../src/models/status'
|
||||
import { GitProcess } from 'git-kitchen-sink'
|
||||
|
||||
const fs = require('fs-extra')
|
||||
import * as fs from 'fs-extra'
|
||||
const temp = require('temp').track()
|
||||
|
||||
describe('git/status', () => {
|
||||
|
|
|
@ -4,10 +4,12 @@ const expect = chai.expect
|
|||
import { groupedAndFilteredBranches } from '../../src/ui/branches/grouped-and-filtered-branches'
|
||||
import { Branch, BranchType } from '../../src/models/branch'
|
||||
import { Commit } from '../../src/models/commit'
|
||||
import { CommitIdentity } from '../../src/models/commit-identity'
|
||||
|
||||
describe('Branches grouping', () => {
|
||||
|
||||
const commit = new Commit('300acef', 'summary', 'body', 'Hubot', 'hubot@github.com', new Date(), [])
|
||||
const author = new CommitIdentity('Hubot', 'hubot@github.com', new Date())
|
||||
const commit = new Commit('300acef', 'summary', 'body', author, [])
|
||||
|
||||
const currentBranch = new Branch('master', null, commit, BranchType.Local)
|
||||
const defaultBranch = new Branch('master', null, commit, BranchType.Local)
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
"electron-winstaller": "^2.3.0",
|
||||
"express": "^4.13.4",
|
||||
"extract-text-webpack-plugin": "^1.0.1",
|
||||
"fs-extra": "^0.30.0",
|
||||
"fs-extra": "^1.0.0",
|
||||
"got": "^6.3.0",
|
||||
"html-webpack-plugin": "^2.21.0",
|
||||
"mocha": "^3.0.2",
|
||||
|
@ -76,6 +76,7 @@
|
|||
"@types/electron": "^1.3.15",
|
||||
"@types/electron-window-state": "^2.0.27",
|
||||
"@types/event-kit": "^1.2.28",
|
||||
"@types/fs-extra": "0.0.35",
|
||||
"@types/keytar": "^3.0.28",
|
||||
"@types/mocha": "^2.2.29",
|
||||
"@types/node": "^6.0.31",
|
||||
|
|
Loading…
Reference in a new issue