Skip to content
This repository was archived by the owner on Dec 15, 2022. It is now read-only.
Merged
31 changes: 27 additions & 4 deletions lib/controllers/git-panel-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default class GitPanelController {
this.pull = this.pull.bind(this)
this.fetch = this.fetch.bind(this)
this.commit = this.commit.bind(this)
this.setAmending = this.setAmending.bind(this)
this.checkout = this.checkout.bind(this)
this.abortMerge = this.abortMerge.bind(this)
this.switchRepository(props.repository)
Expand Down Expand Up @@ -47,7 +48,9 @@ export default class GitPanelController {
stageFiles={this.stageFiles}
unstageFiles={this.unstageFiles}
commit={this.commit}
setAmending={this.setAmending}
isMerging={this.isMerging}
isAmending={this.isAmending}
abortMerge={this.abortMerge}
push={this.push}
pull={this.pull}
Expand Down Expand Up @@ -87,7 +90,7 @@ export default class GitPanelController {
async _refreshModelData (repository) {
if (repository) {
this.unstagedChanges = await repository.getUnstagedChanges()
this.stagedChanges = await repository.getStagedChanges()
await this._refreshStagedChanges(repository)
this.mergeConflicts = await repository.getMergeConflicts()
this.lastCommit = await repository.getLastCommit()
this.isMerging = await repository.isMerging()
Expand All @@ -111,6 +114,14 @@ export default class GitPanelController {
return etch.update(this)
}

async _refreshStagedChanges (repository) {
if (this.isAmending) {
this.stagedChanges = await repository.getStagedChangesSinceParentCommit()
} else {
this.stagedChanges = await repository.getStagedChanges()
}
}

unstageFilePatch (filePatch) {
return this.repository.applyPatchToIndex(filePatch.getUnstagePatch())
}
Expand All @@ -132,7 +143,11 @@ export default class GitPanelController {
}

unstageFiles (filePaths) {
return this.repository.unstageFiles(filePaths)
if (this.isAmending) {
return this.repository.stageFilesFromParentCommit(filePaths)
} else {
return this.repository.unstageFiles(filePaths)
}
}

push () {
Expand All @@ -155,8 +170,16 @@ export default class GitPanelController {
return this.stagedChanges.length === 0 && this.unstagedChanges.length === 0
}

commit (message, options) {
return this.repository.commit(message, options)
async commit (message, options) {
await this.repository.commit(message, options)
this.setAmending(false)
}

async setAmending (isAmending) {
if (this.props.didChangeAmending) this.props.didChangeAmending()
this.isAmending = isAmending
await this._refreshStagedChanges(this.repository)
return etch.update(this)
}

abortMerge () {
Expand Down
5 changes: 3 additions & 2 deletions lib/git-shell-out-strategy.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ export default class GitShellOutStrategy {
return this.exec(args)
}

unstageFiles (paths) {
unstageFiles (paths, commit = 'HEAD') {
if (paths.length === 0) return
const args = ['reset', '--'].concat(paths)
const args = ['reset', commit, '--' ].concat(paths)
return this.exec(args)
}

Expand Down Expand Up @@ -98,6 +98,7 @@ export default class GitShellOutStrategy {
async diff (options = {}) {
let args = ['diff', '--no-prefix', '--no-renames', '--diff-filter=u']
if (options.staged) args.push('--staged')
if (options.baseCommit) args.push(options.baseCommit)
let output = await this.exec(args)

let rawDiffs = []
Expand Down
10 changes: 9 additions & 1 deletion lib/github-package.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export default class GithubPackage {
commandRegistry,
notificationManager,
didSelectFilePatch: this.didSelectFilePatch.bind(this),
didSelectMergeConflictFile: this.didSelectMergeConflictFile.bind(this)
didSelectMergeConflictFile: this.didSelectMergeConflictFile.bind(this),
didChangeAmending: this.didChangeAmending.bind(this)
})
this.changedFilesCountController = new ChangedFilesCountController({
repository: null, didClick: this.toggleGitPanel.bind(this)
Expand Down Expand Up @@ -97,6 +98,13 @@ export default class GithubPackage {
}
}

didChangeAmending () {
if (this.filePatchController && this.filePatchController.props.stagingStatus === 'staged') {
this.filePatchController.destroy()
this.filePatchController = null
}
}

toggleGitPanel () {
if (this.workspaceGitPanel) {
this.workspaceGitPanel.destroy()
Expand Down
4 changes: 4 additions & 0 deletions lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export function fsStat (absoluteFilePath) {
})
}

export function shortenSha (sha) {
return sha.slice(0, 8)
}

export const classNameForStatus = {
added: 'added',
deleted: 'removed',
Expand Down
105 changes: 60 additions & 45 deletions lib/models/repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export default class Repository {
this.workingDirectory = workingDirectory
this.emitter = new Emitter()
this.stagedFilePatchesByPath = new Map()
this.stagedFilePatchesSinceParentCommitByPath = new Map()
this.unstagedFilePatchesByPath = new Map()
this.mergeConflictsByPath = new Map()
this.git = gitStrategy
Expand Down Expand Up @@ -76,86 +77,94 @@ export default class Repository {

async refresh () {
if (global.PRINT_GIT_TIMES) console.time('refresh')
await this.refreshStagedChanges()
await this.refreshUnstagedChanges()
await this.refreshMergeConflicts()
this.stagedChangesPromise = this.fetchStagedChanges()
this.stagedChangesSinceParentCommitPromise = this.fetchStagedChangesSinceParentCommit()
this.unstagedChangesPromise = this.fetchUnstagedChanges()
this.mergeConflictsPromise = this.fetchMergeConflicts()
await Promise.all([
this.stagedChangesPromise,
this.stagedChangesSinceParentCommitPromise,
this.unstagedChangesPromise,
this.mergeConflictsPromise
])
if (global.PRINT_GIT_TIMES) console.timeEnd('refresh')
this.didUpdate()
}

getUnstagedChanges () {
return this.unstagedChangesPromise || this.refreshUnstagedChanges()
if (!this.unstagedChangesPromise) {
this.unstagedChangesPromise = this.fetchUnstagedChanges()
}
return this.unstagedChangesPromise
}

getStagedChanges () {
return this.stagedChangesPromise || this.refreshStagedChanges()
if (!this.stagedChangesPromise) {
this.stagedChangesPromise = this.fetchStagedChanges()
}
return this.stagedChangesPromise
}

getMergeConflicts () {
return this.mergeConflictsPromise || this.refreshMergeConflicts()
getStagedChangesSinceParentCommit () {
if (!this.stagedChangesSinceParentCommitPromise) {
this.stagedChangesSinceParentCommitPromise = this.fetchStagedChangesSinceParentCommit()
}
return this.stagedChangesSinceParentCommitPromise
}

refreshUnstagedChanges () {
this.unstagedChangesPromise = this.fetchUnstagedChanges()
return this.unstagedChangesPromise
getMergeConflicts () {
if (!this.mergeConflictsPromise) {
this.mergeConflictsPromise = this.fetchMergeConflicts()
}
return this.mergeConflictsPromise
}

refreshStagedChanges () {
this.stagedChangesPromise = this.fetchStagedChanges()
return this.stagedChangesPromise
async fetchUnstagedChanges () {
const rawDiffs = await this.git.diff()
return this.updateFilePatches(this.unstagedFilePatchesByPath, rawDiffs)
}

refreshMergeConflicts () {
this.mergeConflictsPromise = this.fetchMergeConflicts()
return this.mergeConflictsPromise
async fetchStagedChanges () {
const rawDiffs = await this.git.diff({staged: true})
return this.updateFilePatches(this.stagedFilePatchesByPath, rawDiffs)
}

async fetchUnstagedChanges () {
const rawDiffs = await this.git.diff()
const validFilePatches = new Set()
for (let newPatch of this.buildFilePatchesFromRawDiffs(rawDiffs)) {
const path = newPatch.getPath()
const existingPatch = this.unstagedFilePatchesByPath.get(path)
if (existingPatch == null) {
this.unstagedFilePatchesByPath.set(path, newPatch)
async fetchStagedChangesSinceParentCommit () {
try {
const rawDiffs = await this.git.diff({staged: true, baseCommit: 'HEAD~'})
return this.updateFilePatches(this.stagedFilePatchesSinceParentCommitByPath, rawDiffs)
} catch (e) {
if (e.message.includes(`ambiguous argument 'HEAD~'`)) {
return []
} else {
existingPatch.update(newPatch)
}
validFilePatches.add(path)
}

for (let [path, patch] of this.unstagedFilePatchesByPath) {
if (!validFilePatches.has(path)) {
this.unstagedFilePatchesByPath.delete(path)
patch.destroy()
throw e
}
}
return Array.from(this.unstagedFilePatchesByPath.values())
.sort((a, b) => a.getPath().localeCompare(b.getPath()))
}

async fetchStagedChanges () {
const rawDiffs = await this.git.diff({staged: true})
updateFilePatches (patchesByPath, rawDiffs) {
const filePatches = this.buildFilePatchesFromRawDiffs(rawDiffs)

const validFilePatches = new Set()
for (let newPatch of this.buildFilePatchesFromRawDiffs(rawDiffs)) {
for (let newPatch of filePatches) {
const path = newPatch.getPath()
const existingPatch = this.stagedFilePatchesByPath.get(path)
const existingPatch = patchesByPath.get(path)
if (existingPatch == null) {
this.stagedFilePatchesByPath.set(path, newPatch)
patchesByPath.set(path, newPatch)
} else {
existingPatch.update(newPatch)
}
validFilePatches.add(path)
}

for (let [path, patch] of this.stagedFilePatchesByPath) {
for (let [path, patch] of patchesByPath) {
if (!validFilePatches.has(path)) {
this.stagedFilePatchesByPath.delete(path)
patchesByPath.delete(path)
patch.destroy()
}
}
return Array.from(this.stagedFilePatchesByPath.values())
.sort((a, b) => a.getPath().localeCompare(b.getPath()))

return Array.from(patchesByPath.values()).sort((a, b) => a.getPath().localeCompare(b.getPath()))
}

buildFilePatchesFromRawDiffs (rawDiffs) {
Expand Down Expand Up @@ -228,6 +237,11 @@ export default class Repository {
await this.refresh()
}

async stageFilesFromParentCommit (paths) {
await this.git.unstageFiles(paths, 'HEAD~')
await this.refresh()
}

async applyPatchToIndex (filePatch) {
const patchStr = filePatch.getHeaderString() + filePatch.toString()
await this.git.applyPatchToIndex(patchStr)
Expand Down Expand Up @@ -268,7 +282,8 @@ export default class Repository {

async commit (message, options) {
await this.git.commit(this.stripCommitComments(message), options)
this.refreshStagedChanges()
this.stagedChangesPromise = null
this.stagedChangesSinceParentCommitPromise = null
this.didUpdate()
}

Expand Down
47 changes: 36 additions & 11 deletions lib/views/commit-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import {CompositeDisposable} from 'atom'
import etch from 'etch'
import {shortenSha} from '../helpers'

export default class CommitView {
constructor (props) {
Expand Down Expand Up @@ -64,7 +65,7 @@ export default class CommitView {
<label className='git-CommitView-label input-label' style={{display: this.props.isMerging ? 'none' : ''}}><input ref='amend' className='input-checkbox' type='checkbox' onclick={this.handleAmendBoxClick}/> Amend </label>
<button ref='commitButton' className='btn git-CommitView-button'
onclick={this.commit.bind(this)}
disabled={!this.isCommitButtonEnabled()}>Commit {this.props.branchName ? `to ${this.props.branchName}` : ''}</button>
disabled={!this.isCommitButtonEnabled()}>{this.commitButtonText()}</button>
<div ref='remainingCharacters' className={`git-CommitView-remaining-characters ${remainingCharactersClassName}`}>
{this.getRemainingCharacters()}
</div>
Expand Down Expand Up @@ -92,18 +93,30 @@ export default class CommitView {
return etch.update(this)
}

handleAmendBoxClick () {
async handleAmendBoxClick () {
const checked = this.refs.amend.checked
if (this.editor.getText()) {
const choice = atom.confirm({
message: 'Would you like to discard the current commit message?',
detailedMessage: checked ? 'It will be replaced with the most recent commit message' : '',
buttons: ['Replace', 'Keep current']
})
if (choice !== 0) return
const currentMessage = this.editor.getText()
const lastCommitMessage = this.props.lastCommit ? this.props.lastCommit.message : ''

if (this.props.setAmending) await this.props.setAmending(checked)

if (checked) {
if (currentMessage && currentMessage !== lastCommitMessage) {
const choice = atom.confirm({
message: 'Would you like to discard the current commit message?',
detailedMessage: checked ? 'It will be replaced with the most recent commit message' : '',
buttons: ['Keep current', 'Replace']
})
if (choice !== 0) {
this.editor.setText(lastCommitMessage)
}
} else {
this.editor.setText(lastCommitMessage)
}
} else if (currentMessage === lastCommitMessage) {
this.editor.setText('')
}
const text = (checked && this.props.lastCommit) ? this.props.lastCommit.message : ''
this.editor.setText(text)

return etch.update(this)
}

Expand Down Expand Up @@ -138,4 +151,16 @@ export default class CommitView {
isCommitButtonEnabled () {
return this.props.stagedChangesExist && !this.props.mergeConflictsExist && this.editor && this.editor.getText().length !== 0
}

commitButtonText () {
if (this.props.isAmending) {
return `Amend commit ${shortenSha(this.props.lastCommit.sha)})`
} else {
if (this.props.branchName) {
return `Commit to ${this.props.branchName}`
} else {
return 'Commit'
}
}
}
}
4 changes: 4 additions & 0 deletions lib/views/git-panel-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,16 @@ export default class GitPanelView {
didSelectMergeConflictFile={this.props.didSelectMergeConflictFile}
stageFiles={this.props.stageFiles}
unstageFiles={this.props.unstageFiles}
lastCommit={this.props.lastCommit}
isAmending={this.props.isAmending}
/>
<CommitView
ref='commitView'
workspace={this.props.workspace}
stagedChangesExist={this.props.stagedChanges.length > 0}
mergeConflictsExist={this.props.mergeConflicts.length > 0}
commit={this.props.commit}
setAmending={this.props.setAmending}
disableCommitButton={this}
abortMerge={this.props.abortMerge}
branchName={this.props.branchName}
Expand All @@ -67,6 +70,7 @@ export default class GitPanelView {
maximumCharacterLimit={72}
message={this.props.mergeMessage}
isMerging={this.props.isMerging}
isAmending={this.props.isAmending}
lastCommit={this.props.lastCommit}
/>
</div>
Expand Down
Loading