@theia/scm 1.45.1 → 1.46.0-next.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -31
- package/lib/browser/decorations/scm-decorations-service.d.ts +14 -14
- package/lib/browser/decorations/scm-decorations-service.js +101 -101
- package/lib/browser/decorations/scm-navigator-decorator.d.ts +25 -25
- package/lib/browser/decorations/scm-navigator-decorator.js +132 -132
- package/lib/browser/decorations/scm-tab-bar-decorator.d.ts +17 -17
- package/lib/browser/decorations/scm-tab-bar-decorator.js +93 -93
- package/lib/browser/dirty-diff/content-lines.d.ts +12 -12
- package/lib/browser/dirty-diff/content-lines.js +106 -106
- package/lib/browser/dirty-diff/content-lines.spec.d.ts +1 -1
- package/lib/browser/dirty-diff/content-lines.spec.js +39 -39
- package/lib/browser/dirty-diff/diff-computer.d.ts +29 -29
- package/lib/browser/dirty-diff/diff-computer.js +102 -102
- package/lib/browser/dirty-diff/diff-computer.spec.d.ts +1 -1
- package/lib/browser/dirty-diff/diff-computer.spec.js +315 -315
- package/lib/browser/dirty-diff/dirty-diff-decorator.d.ts +14 -14
- package/lib/browser/dirty-diff/dirty-diff-decorator.js +98 -98
- package/lib/browser/dirty-diff/dirty-diff-module.d.ts +3 -3
- package/lib/browser/dirty-diff/dirty-diff-module.js +24 -24
- package/lib/browser/scm-amend-component.d.ts +123 -123
- package/lib/browser/scm-amend-component.js +463 -463
- package/lib/browser/scm-amend-widget.d.ts +20 -20
- package/lib/browser/scm-amend-widget.js +101 -101
- package/lib/browser/scm-avatar-service.d.ts +3 -3
- package/lib/browser/scm-avatar-service.js +36 -36
- package/lib/browser/scm-commit-widget.d.ts +52 -52
- package/lib/browser/scm-commit-widget.js +199 -199
- package/lib/browser/scm-context-key-service.d.ts +10 -10
- package/lib/browser/scm-context-key-service.js +58 -58
- package/lib/browser/scm-contribution.d.ts +83 -83
- package/lib/browser/scm-contribution.js +356 -356
- package/lib/browser/scm-frontend-module.d.ts +6 -6
- package/lib/browser/scm-frontend-module.js +130 -130
- package/lib/browser/scm-groups-tree-model.d.ts +14 -14
- package/lib/browser/scm-groups-tree-model.js +97 -97
- package/lib/browser/scm-input.d.ts +53 -53
- package/lib/browser/scm-input.js +127 -127
- package/lib/browser/scm-layout-migrations.d.ts +9 -9
- package/lib/browser/scm-layout-migrations.js +79 -79
- package/lib/browser/scm-no-repository-widget.d.ts +8 -8
- package/lib/browser/scm-no-repository-widget.js +49 -49
- package/lib/browser/scm-preferences.d.ts +11 -11
- package/lib/browser/scm-preferences.js +51 -51
- package/lib/browser/scm-provider.d.ts +58 -58
- package/lib/browser/scm-provider.js +19 -19
- package/lib/browser/scm-quick-open-service.d.ts +11 -11
- package/lib/browser/scm-quick-open-service.js +73 -73
- package/lib/browser/scm-repository.d.ts +17 -17
- package/lib/browser/scm-repository.js +41 -41
- package/lib/browser/scm-service.d.ts +26 -26
- package/lib/browser/scm-service.js +108 -108
- package/lib/browser/scm-tree-label-provider.d.ts +7 -7
- package/lib/browser/scm-tree-label-provider.js +57 -57
- package/lib/browser/scm-tree-model.d.ts +74 -74
- package/lib/browser/scm-tree-model.js +351 -351
- package/lib/browser/scm-tree-widget.d.ts +208 -208
- package/lib/browser/scm-tree-widget.js +703 -703
- package/lib/browser/scm-widget.d.ts +40 -40
- package/lib/browser/scm-widget.js +218 -218
- package/package.json +6 -6
- package/src/browser/decorations/scm-decorations-service.ts +78 -78
- package/src/browser/decorations/scm-navigator-decorator.ts +121 -121
- package/src/browser/decorations/scm-tab-bar-decorator.ts +83 -83
- package/src/browser/dirty-diff/content-lines.spec.ts +42 -42
- package/src/browser/dirty-diff/content-lines.ts +112 -112
- package/src/browser/dirty-diff/diff-computer.spec.ts +387 -387
- package/src/browser/dirty-diff/diff-computer.ts +129 -129
- package/src/browser/dirty-diff/dirty-diff-decorator.ts +107 -107
- package/src/browser/dirty-diff/dirty-diff-module.ts +24 -24
- package/src/browser/scm-amend-component.tsx +600 -600
- package/src/browser/scm-amend-widget.tsx +77 -77
- package/src/browser/scm-avatar-service.ts +27 -27
- package/src/browser/scm-commit-widget.tsx +215 -215
- package/src/browser/scm-context-key-service.ts +46 -46
- package/src/browser/scm-contribution.ts +361 -361
- package/src/browser/scm-frontend-module.ts +149 -149
- package/src/browser/scm-groups-tree-model.ts +78 -78
- package/src/browser/scm-input.ts +164 -164
- package/src/browser/scm-layout-migrations.ts +64 -64
- package/src/browser/scm-no-repository-widget.tsx +41 -41
- package/src/browser/scm-preferences.ts +63 -63
- package/src/browser/scm-provider.ts +91 -91
- package/src/browser/scm-quick-open-service.ts +48 -48
- package/src/browser/scm-repository.ts +52 -52
- package/src/browser/scm-service.ts +108 -108
- package/src/browser/scm-tree-label-provider.ts +44 -44
- package/src/browser/scm-tree-model.ts +405 -405
- package/src/browser/scm-tree-widget.tsx +838 -838
- package/src/browser/scm-widget.tsx +204 -204
- package/src/browser/style/dirty-diff-decorator.css +52 -52
- package/src/browser/style/dirty-diff.css +50 -50
- package/src/browser/style/index.css +271 -271
- package/src/browser/style/scm-amend-component.css +94 -94
- package/src/browser/style/scm.svg +4 -4
|
@@ -1,600 +1,600 @@
|
|
|
1
|
-
// *****************************************************************************
|
|
2
|
-
// Copyright (C) 2019 Arm and others.
|
|
3
|
-
//
|
|
4
|
-
// This program and the accompanying materials are made available under the
|
|
5
|
-
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
-
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
-
//
|
|
8
|
-
// This Source Code may also be made available under the following Secondary
|
|
9
|
-
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
-
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
-
// with the GNU Classpath Exception which is available at
|
|
12
|
-
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
-
//
|
|
14
|
-
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
-
// *****************************************************************************
|
|
16
|
-
|
|
17
|
-
import '../../src/browser/style/scm-amend-component.css';
|
|
18
|
-
|
|
19
|
-
import * as React from '@theia/core/shared/react';
|
|
20
|
-
import { ScmAvatarService } from './scm-avatar-service';
|
|
21
|
-
import { codicon, StorageService } from '@theia/core/lib/browser';
|
|
22
|
-
import { Disposable, DisposableCollection } from '@theia/core';
|
|
23
|
-
|
|
24
|
-
import { ScmRepository } from './scm-repository';
|
|
25
|
-
import { ScmAmendSupport, ScmCommit } from './scm-provider';
|
|
26
|
-
import { nls } from '@theia/core/lib/common/nls';
|
|
27
|
-
|
|
28
|
-
export interface ScmAmendComponentProps {
|
|
29
|
-
style: React.CSSProperties | undefined,
|
|
30
|
-
repository: ScmRepository,
|
|
31
|
-
scmAmendSupport: ScmAmendSupport,
|
|
32
|
-
setCommitMessage: (message: string) => void,
|
|
33
|
-
avatarService: ScmAvatarService,
|
|
34
|
-
storageService: StorageService,
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface ScmAmendComponentState {
|
|
38
|
-
/**
|
|
39
|
-
* This is used for transitioning. When setting up a transition, we first set to render
|
|
40
|
-
* the elements in their starting positions. This includes creating the elements to be
|
|
41
|
-
* transitioned in, even though those controls will not be visible when state is 'start'.
|
|
42
|
-
* On the next frame after 'start', we render elements with their final positions and with
|
|
43
|
-
* the transition properties.
|
|
44
|
-
*/
|
|
45
|
-
transition: {
|
|
46
|
-
state: 'none'
|
|
47
|
-
} | {
|
|
48
|
-
state: 'start' | 'transitioning',
|
|
49
|
-
direction: 'up' | 'down',
|
|
50
|
-
previousLastCommit: { commit: ScmCommit, avatar: string }
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
amendingCommits: { commit: ScmCommit, avatar: string }[];
|
|
54
|
-
lastCommit: { commit: ScmCommit, avatar: string } | undefined;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const TRANSITION_TIME_MS = 300;
|
|
58
|
-
const REPOSITORY_STORAGE_KEY = 'scmRepository';
|
|
59
|
-
|
|
60
|
-
export class ScmAmendComponent extends React.Component<ScmAmendComponentProps, ScmAmendComponentState> {
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* a hint on how to animate an update, set by certain user action handlers
|
|
64
|
-
* and used when updating the view based on a repository change
|
|
65
|
-
*/
|
|
66
|
-
protected transitionHint: 'none' | 'amend' | 'unamend' = 'none';
|
|
67
|
-
|
|
68
|
-
protected lastCommitHeight: number = 0;
|
|
69
|
-
lastCommitScrollRef = (instance: HTMLDivElement) => {
|
|
70
|
-
if (instance && this.lastCommitHeight === 0) {
|
|
71
|
-
this.lastCommitHeight = instance.getBoundingClientRect().height;
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
constructor(props: ScmAmendComponentProps) {
|
|
76
|
-
super(props);
|
|
77
|
-
|
|
78
|
-
this.state = {
|
|
79
|
-
transition: { state: 'none' },
|
|
80
|
-
amendingCommits: [],
|
|
81
|
-
lastCommit: undefined
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const setState = this.setState.bind(this);
|
|
85
|
-
this.setState = newState => {
|
|
86
|
-
if (!this.toDisposeOnUnmount.disposed) {
|
|
87
|
-
setState(newState);
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
protected readonly toDisposeOnUnmount = new DisposableCollection();
|
|
93
|
-
|
|
94
|
-
override async componentDidMount(): Promise<void> {
|
|
95
|
-
this.toDisposeOnUnmount.push(Disposable.create(() => { /* mark as mounted */ }));
|
|
96
|
-
|
|
97
|
-
const lastCommit = await this.getLastCommit();
|
|
98
|
-
this.setState({ amendingCommits: await this.buildAmendingList(lastCommit ? lastCommit.commit : undefined), lastCommit });
|
|
99
|
-
|
|
100
|
-
if (this.toDisposeOnUnmount.disposed) {
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
this.toDisposeOnUnmount.push(
|
|
104
|
-
this.props.repository.provider.onDidChange(() => this.fetchStatusAndSetState())
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
override componentWillUnmount(): void {
|
|
109
|
-
this.toDisposeOnUnmount.dispose();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async fetchStatusAndSetState(): Promise<void> {
|
|
113
|
-
const storageKey = this.getStorageKey();
|
|
114
|
-
|
|
115
|
-
const nextCommit = await this.getLastCommit();
|
|
116
|
-
if (nextCommit && this.state.lastCommit && nextCommit.commit.id === this.state.lastCommit.commit.id) {
|
|
117
|
-
// No change here
|
|
118
|
-
} else if (nextCommit === undefined && this.state.lastCommit === undefined) {
|
|
119
|
-
// No change here
|
|
120
|
-
} else if (this.transitionHint === 'none') {
|
|
121
|
-
// If the 'last' commit changes, but we are not expecting an 'amend'
|
|
122
|
-
// or 'unamend' to occur, then we clear out the list of amended commits.
|
|
123
|
-
// This is because an unexpected change has happened to the repository,
|
|
124
|
-
// perhaps the user committed, merged, or something. The amended commits
|
|
125
|
-
// will no longer be valid.
|
|
126
|
-
|
|
127
|
-
// Note that there may or may not have been a previous lastCommit (if the
|
|
128
|
-
// repository was previously empty with no initial commit then lastCommit
|
|
129
|
-
// will be undefined). Either way we clear the amending commits.
|
|
130
|
-
await this.clearAmendingCommits();
|
|
131
|
-
|
|
132
|
-
// There is a change to the last commit, but no transition hint so
|
|
133
|
-
// the view just updates without transition.
|
|
134
|
-
this.setState({ amendingCommits: [], lastCommit: nextCommit });
|
|
135
|
-
} else {
|
|
136
|
-
const amendingCommits = this.state.amendingCommits.concat([]); // copy the array
|
|
137
|
-
|
|
138
|
-
const direction: 'up' | 'down' = this.transitionHint === 'amend' ? 'up' : 'down';
|
|
139
|
-
switch (this.transitionHint) {
|
|
140
|
-
case 'amend':
|
|
141
|
-
if (this.state.lastCommit) {
|
|
142
|
-
amendingCommits.push(this.state.lastCommit);
|
|
143
|
-
|
|
144
|
-
const serializedState = JSON.stringify({
|
|
145
|
-
amendingHeadCommitSha: amendingCommits[0].commit.id,
|
|
146
|
-
latestCommitSha: nextCommit ? nextCommit.commit.id : undefined
|
|
147
|
-
});
|
|
148
|
-
this.props.storageService.setData<string | undefined>(storageKey, serializedState);
|
|
149
|
-
}
|
|
150
|
-
break;
|
|
151
|
-
case 'unamend':
|
|
152
|
-
amendingCommits.pop();
|
|
153
|
-
if (amendingCommits.length === 0) {
|
|
154
|
-
this.props.storageService.setData<string | undefined>(storageKey, undefined);
|
|
155
|
-
} else {
|
|
156
|
-
const serializedState = JSON.stringify({
|
|
157
|
-
amendingHeadCommitSha: amendingCommits[0].commit.id,
|
|
158
|
-
latestCommitSha: nextCommit ? nextCommit.commit.id : undefined
|
|
159
|
-
});
|
|
160
|
-
this.props.storageService.setData<string | undefined>(storageKey, serializedState);
|
|
161
|
-
}
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (this.state.lastCommit && nextCommit) {
|
|
166
|
-
const transitionData = { direction, previousLastCommit: this.state.lastCommit };
|
|
167
|
-
this.setState({ lastCommit: nextCommit, amendingCommits, transition: { ...transitionData, state: 'start' } });
|
|
168
|
-
this.onNextFrame(() => {
|
|
169
|
-
this.setState({ transition: { ...transitionData, state: 'transitioning' } });
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
setTimeout(
|
|
173
|
-
() => {
|
|
174
|
-
this.setState({ transition: { state: 'none' } });
|
|
175
|
-
},
|
|
176
|
-
TRANSITION_TIME_MS);
|
|
177
|
-
} else {
|
|
178
|
-
// No previous last commit so no transition
|
|
179
|
-
this.setState({ transition: { state: 'none' }, amendingCommits, lastCommit: nextCommit });
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
this.transitionHint = 'none';
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
private async clearAmendingCommits(): Promise<void> {
|
|
187
|
-
const storageKey = this.getStorageKey();
|
|
188
|
-
await this.props.storageService.setData<string | undefined>(storageKey, undefined);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
private async buildAmendingList(lastCommit: ScmCommit | undefined): Promise<{ commit: ScmCommit, avatar: string }[]> {
|
|
192
|
-
const storageKey = this.getStorageKey();
|
|
193
|
-
const storedState = await this.props.storageService.getData<string | undefined>(storageKey, undefined);
|
|
194
|
-
|
|
195
|
-
// Restore list of commits from saved amending head commit up through parents until the
|
|
196
|
-
// current commit. (If we don't reach the current commit, the repository has been changed in such
|
|
197
|
-
// a way then unamending commits can no longer be done).
|
|
198
|
-
if (storedState) {
|
|
199
|
-
const { amendingHeadCommitSha, latestCommitSha } = JSON.parse(storedState);
|
|
200
|
-
if (!this.commitsAreEqual(lastCommit, latestCommitSha)) {
|
|
201
|
-
// The head commit in the repository has changed. It is not the same commit that was the
|
|
202
|
-
// head commit after the last 'amend'.
|
|
203
|
-
return [];
|
|
204
|
-
}
|
|
205
|
-
const commits = await this.props.scmAmendSupport.getInitialAmendingCommits(amendingHeadCommitSha, lastCommit ? lastCommit.id : undefined);
|
|
206
|
-
|
|
207
|
-
const amendingCommitPromises = commits.map(async commit => {
|
|
208
|
-
const avatar = await this.props.avatarService.getAvatar(commit.authorEmail);
|
|
209
|
-
return { commit, avatar };
|
|
210
|
-
});
|
|
211
|
-
return Promise.all(amendingCommitPromises);
|
|
212
|
-
} else {
|
|
213
|
-
return [];
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
private getStorageKey(): string {
|
|
218
|
-
return REPOSITORY_STORAGE_KEY + ':' + this.props.repository.provider.rootUri;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Commits are equal if the ids are equal or if both are undefined.
|
|
223
|
-
* (If a commit is undefined, it represents the initial empty state of a repository,
|
|
224
|
-
* before the initial commit).
|
|
225
|
-
*/
|
|
226
|
-
private commitsAreEqual(lastCommit: ScmCommit | undefined, savedLastCommitId: string | undefined): boolean {
|
|
227
|
-
return lastCommit
|
|
228
|
-
? lastCommit.id === savedLastCommitId
|
|
229
|
-
: savedLastCommitId === undefined;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* This function will update the 'model' (lastCommit, amendingCommits) only
|
|
234
|
-
* when the repository sees the last commit change.
|
|
235
|
-
* 'render' can be called at any time, so be sure we don't update any 'model'
|
|
236
|
-
* fields until we actually start the transition.
|
|
237
|
-
*/
|
|
238
|
-
protected amend = async (): Promise<void> => {
|
|
239
|
-
if (this.state.transition.state !== 'none' && this.transitionHint !== 'none') {
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
this.transitionHint = 'amend';
|
|
244
|
-
await this.resetAndSetMessage('HEAD~', 'HEAD');
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
protected unamend = async (): Promise<void> => {
|
|
248
|
-
if (this.state.transition.state !== 'none' && this.transitionHint !== 'none') {
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const commitToRestore = (this.state.amendingCommits.length >= 1)
|
|
253
|
-
? this.state.amendingCommits[this.state.amendingCommits.length - 1]
|
|
254
|
-
: undefined;
|
|
255
|
-
const oldestAmendCommit = (this.state.amendingCommits.length >= 2)
|
|
256
|
-
? this.state.amendingCommits[this.state.amendingCommits.length - 2]
|
|
257
|
-
: undefined;
|
|
258
|
-
|
|
259
|
-
if (commitToRestore) {
|
|
260
|
-
const commitToUseForMessage = oldestAmendCommit
|
|
261
|
-
? oldestAmendCommit.commit.id
|
|
262
|
-
: undefined;
|
|
263
|
-
this.transitionHint = 'unamend';
|
|
264
|
-
await this.resetAndSetMessage(commitToRestore.commit.id, commitToUseForMessage);
|
|
265
|
-
}
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
private async resetAndSetMessage(commitToRestore: string, commitToUseForMessage: string | undefined): Promise<void> {
|
|
269
|
-
const message = commitToUseForMessage
|
|
270
|
-
? await this.props.scmAmendSupport.getMessage(commitToUseForMessage)
|
|
271
|
-
: '';
|
|
272
|
-
await this.props.scmAmendSupport.reset(commitToRestore);
|
|
273
|
-
this.props.setCommitMessage(message);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
override render(): JSX.Element {
|
|
277
|
-
const neverShrink = this.state.amendingCommits.length <= 3;
|
|
278
|
-
|
|
279
|
-
const style: React.CSSProperties = neverShrink
|
|
280
|
-
? {
|
|
281
|
-
...this.props.style,
|
|
282
|
-
flexShrink: 0,
|
|
283
|
-
}
|
|
284
|
-
: {
|
|
285
|
-
...this.props.style,
|
|
286
|
-
flexShrink: 1,
|
|
287
|
-
minHeight: 240 // height with three commits
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
return (
|
|
291
|
-
<div className={ScmAmendComponent.Styles.COMMIT_CONTAINER + ' no-select'} style={style}>
|
|
292
|
-
{
|
|
293
|
-
this.state.amendingCommits.length > 0 || (this.state.lastCommit && this.state.transition.state !== 'none' && this.state.transition.direction === 'down')
|
|
294
|
-
? this.renderAmendingCommits()
|
|
295
|
-
: ''
|
|
296
|
-
}
|
|
297
|
-
{
|
|
298
|
-
this.state.lastCommit ?
|
|
299
|
-
<div>
|
|
300
|
-
<div id='lastCommit' className='theia-scm-amend'>
|
|
301
|
-
<div className='theia-header scm-theia-header'>
|
|
302
|
-
{nls.localize('theia/scm/amendHeadCommit', 'HEAD Commit')}
|
|
303
|
-
</div>
|
|
304
|
-
{this.renderLastCommit()}
|
|
305
|
-
</div>
|
|
306
|
-
</div>
|
|
307
|
-
: ''
|
|
308
|
-
}
|
|
309
|
-
</div>
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
protected async getLastCommit(): Promise<{ commit: ScmCommit, avatar: string } | undefined> {
|
|
314
|
-
const commit = await this.props.scmAmendSupport.getLastCommit();
|
|
315
|
-
if (commit) {
|
|
316
|
-
const avatar = await this.props.avatarService.getAvatar(commit.authorEmail);
|
|
317
|
-
return { commit, avatar };
|
|
318
|
-
}
|
|
319
|
-
return undefined;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
protected renderAmendingCommits(): React.ReactNode {
|
|
323
|
-
const neverShrink = this.state.amendingCommits.length <= 3;
|
|
324
|
-
|
|
325
|
-
const style: React.CSSProperties = neverShrink
|
|
326
|
-
? {
|
|
327
|
-
flexShrink: 0,
|
|
328
|
-
}
|
|
329
|
-
: {
|
|
330
|
-
flexShrink: 1,
|
|
331
|
-
// parent minHeight controls height, we just need any value smaller than
|
|
332
|
-
// what the height would be when the parent is at its minHeight
|
|
333
|
-
minHeight: 0
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
return <div id='amendedCommits' className='theia-scm-amend-outer-container' style={style}>
|
|
337
|
-
<div className='theia-header scm-theia-header'>
|
|
338
|
-
<div className='noWrapInfo'>Commits being Amended</div>
|
|
339
|
-
{this.renderAmendCommitListButtons()}
|
|
340
|
-
{this.renderCommitCount(this.state.amendingCommits.length)}
|
|
341
|
-
</div>
|
|
342
|
-
<div style={this.styleAmendedCommits()}>
|
|
343
|
-
{this.state.amendingCommits.map((commitData, index, array) =>
|
|
344
|
-
this.renderCommitBeingAmended(commitData, index === array.length - 1)
|
|
345
|
-
)}
|
|
346
|
-
{
|
|
347
|
-
this.state.lastCommit && this.state.transition.state !== 'none' && this.state.transition.direction === 'down'
|
|
348
|
-
? this.renderCommitBeingAmended(this.state.lastCommit, false)
|
|
349
|
-
: ''
|
|
350
|
-
}
|
|
351
|
-
</div>
|
|
352
|
-
</div>;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
protected renderAmendCommitListButtons(): React.ReactNode {
|
|
356
|
-
return <div className='theia-scm-inline-actions-container'>
|
|
357
|
-
<div className='theia-scm-inline-actions'>
|
|
358
|
-
<div className='theia-scm-inline-action'>
|
|
359
|
-
<a className={codicon('dash')} title='Unamend All Commits' onClick={this.unamendAll} />
|
|
360
|
-
</div>
|
|
361
|
-
<div className='theia-scm-inline-action' >
|
|
362
|
-
<a className={codicon('close')} title='Clear Amending Commits' onClick={this.clearAmending} />
|
|
363
|
-
</div>
|
|
364
|
-
</div>
|
|
365
|
-
</div>;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
protected renderLastCommit(): React.ReactNode {
|
|
369
|
-
if (!this.state.lastCommit) {
|
|
370
|
-
return '';
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const canAmend: boolean = true;
|
|
374
|
-
return <div className={ScmAmendComponent.Styles.COMMIT_AND_BUTTON} style={{ flexGrow: 0, flexShrink: 0 }} key={this.state.lastCommit.commit.id}>
|
|
375
|
-
{this.renderLastCommitNoButton(this.state.lastCommit)}
|
|
376
|
-
{
|
|
377
|
-
canAmend
|
|
378
|
-
? <div className={ScmAmendComponent.Styles.FLEX_CENTER}>
|
|
379
|
-
<button className='theia-button' title={nls.localize('theia/scm/amendLastCommit', 'Amend last commit')} onClick={this.amend}>
|
|
380
|
-
{nls.localize('theia/scm/amend', 'Amend')}
|
|
381
|
-
</button>
|
|
382
|
-
</div>
|
|
383
|
-
: ''
|
|
384
|
-
}
|
|
385
|
-
</div>;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
protected renderLastCommitNoButton(lastCommit: { commit: ScmCommit, avatar: string }): React.ReactNode {
|
|
389
|
-
switch (this.state.transition.state) {
|
|
390
|
-
case 'none':
|
|
391
|
-
return <div ref={this.lastCommitScrollRef} className='theia-scm-scrolling-container'>
|
|
392
|
-
{this.renderCommitAvatarAndDetail(lastCommit)}
|
|
393
|
-
</div>;
|
|
394
|
-
|
|
395
|
-
case 'start':
|
|
396
|
-
case 'transitioning':
|
|
397
|
-
switch (this.state.transition.direction) {
|
|
398
|
-
case 'up':
|
|
399
|
-
return <div style={this.styleLastCommitMovingUp(this.state.transition.state)}>
|
|
400
|
-
{this.renderCommitAvatarAndDetail(this.state.transition.previousLastCommit)}
|
|
401
|
-
{this.renderCommitAvatarAndDetail(lastCommit)}
|
|
402
|
-
</div>;
|
|
403
|
-
case 'down':
|
|
404
|
-
return <div style={this.styleLastCommitMovingDown(this.state.transition.state)}>
|
|
405
|
-
{this.renderCommitAvatarAndDetail(lastCommit)}
|
|
406
|
-
{this.renderCommitAvatarAndDetail(this.state.transition.previousLastCommit)}
|
|
407
|
-
</div>;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* See https://stackoverflow.com/questions/26556436/react-after-render-code
|
|
414
|
-
*
|
|
415
|
-
* @param callback
|
|
416
|
-
*/
|
|
417
|
-
protected onNextFrame(callback: FrameRequestCallback): void {
|
|
418
|
-
setTimeout(
|
|
419
|
-
() => window.requestAnimationFrame(callback),
|
|
420
|
-
0);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
protected renderCommitAvatarAndDetail(commitData: { commit: ScmCommit, avatar: string }): React.ReactNode {
|
|
424
|
-
const { commit, avatar } = commitData;
|
|
425
|
-
return <div className={ScmAmendComponent.Styles.COMMIT_AVATAR_AND_TEXT} key={commit.id}>
|
|
426
|
-
<div className={ScmAmendComponent.Styles.COMMIT_MESSAGE_AVATAR}>
|
|
427
|
-
<img src={avatar} />
|
|
428
|
-
</div>
|
|
429
|
-
<div className={ScmAmendComponent.Styles.COMMIT_DETAILS}>
|
|
430
|
-
<div className={ScmAmendComponent.Styles.COMMIT_MESSAGE_SUMMARY}>{commit.summary}</div>
|
|
431
|
-
<div className={ScmAmendComponent.Styles.LAST_COMMIT_MESSAGE_TIME}>{`${commit.authorDateRelative} by ${commit.authorName}`}</div>
|
|
432
|
-
</div>
|
|
433
|
-
</div>;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
protected renderCommitCount(commits: number): React.ReactNode {
|
|
437
|
-
return <div className='notification-count-container scm-change-count'>
|
|
438
|
-
<span className='notification-count'>{commits}</span>
|
|
439
|
-
</div>;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
protected renderCommitBeingAmended(commitData: { commit: ScmCommit, avatar: string }, isOldestAmendCommit: boolean): JSX.Element {
|
|
443
|
-
if (isOldestAmendCommit && this.state.transition.state !== 'none' && this.state.transition.direction === 'up') {
|
|
444
|
-
return <div className={ScmAmendComponent.Styles.COMMIT_AVATAR_AND_TEXT} style={{ flexGrow: 0, flexShrink: 0 }} key={commitData.commit.id}>
|
|
445
|
-
<div className='fixed-height-commit-container'>
|
|
446
|
-
{this.renderCommitAvatarAndDetail(commitData)}
|
|
447
|
-
</div>
|
|
448
|
-
</div>;
|
|
449
|
-
} else {
|
|
450
|
-
return <div className={ScmAmendComponent.Styles.COMMIT_AVATAR_AND_TEXT} style={{ flexGrow: 0, flexShrink: 0 }} key={commitData.commit.id}>
|
|
451
|
-
{this.renderCommitAvatarAndDetail(commitData)}
|
|
452
|
-
{
|
|
453
|
-
isOldestAmendCommit
|
|
454
|
-
? <div className={ScmAmendComponent.Styles.FLEX_CENTER}>
|
|
455
|
-
<button className='theia-button' title={nls.localize('theia/scm/unamendCommit', 'Unamend commit')} onClick={this.unamend}>
|
|
456
|
-
{nls.localize('theia/scm/unamend', 'Unamend')}
|
|
457
|
-
</button>
|
|
458
|
-
</div>
|
|
459
|
-
: ''
|
|
460
|
-
}
|
|
461
|
-
</div>;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/*
|
|
466
|
-
* The style for the <div> containing the list of commits being amended.
|
|
467
|
-
* This div is scrollable.
|
|
468
|
-
*/
|
|
469
|
-
protected styleAmendedCommits(): React.CSSProperties {
|
|
470
|
-
const base = {
|
|
471
|
-
display: 'flex',
|
|
472
|
-
whitespace: 'nowrap',
|
|
473
|
-
width: '100%',
|
|
474
|
-
minHeight: 0,
|
|
475
|
-
flexShrink: 1,
|
|
476
|
-
paddingTop: '2px',
|
|
477
|
-
};
|
|
478
|
-
|
|
479
|
-
switch (this.state.transition.state) {
|
|
480
|
-
case 'none':
|
|
481
|
-
return {
|
|
482
|
-
...base,
|
|
483
|
-
flexDirection: 'column',
|
|
484
|
-
overflowY: 'auto',
|
|
485
|
-
marginBottom: '0',
|
|
486
|
-
};
|
|
487
|
-
case 'start':
|
|
488
|
-
case 'transitioning':
|
|
489
|
-
let startingMargin: number = 0;
|
|
490
|
-
let endingMargin: number = 0;
|
|
491
|
-
switch (this.state.transition.direction) {
|
|
492
|
-
case 'down':
|
|
493
|
-
startingMargin = 0;
|
|
494
|
-
endingMargin = -32;
|
|
495
|
-
break;
|
|
496
|
-
case 'up':
|
|
497
|
-
startingMargin = -32;
|
|
498
|
-
endingMargin = 0;
|
|
499
|
-
break;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
switch (this.state.transition.state) {
|
|
503
|
-
case 'start':
|
|
504
|
-
return {
|
|
505
|
-
...base,
|
|
506
|
-
flexDirection: 'column',
|
|
507
|
-
overflowY: 'hidden',
|
|
508
|
-
marginBottom: `${startingMargin}px`,
|
|
509
|
-
};
|
|
510
|
-
case 'transitioning':
|
|
511
|
-
return {
|
|
512
|
-
...base,
|
|
513
|
-
flexDirection: 'column',
|
|
514
|
-
overflowY: 'hidden',
|
|
515
|
-
marginBottom: `${endingMargin}px`,
|
|
516
|
-
transitionProperty: 'margin-bottom',
|
|
517
|
-
transitionDuration: `${TRANSITION_TIME_MS}ms`,
|
|
518
|
-
transitionTimingFunction: 'linear'
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
protected styleLastCommitMovingUp(transitionState: 'start' | 'transitioning'): React.CSSProperties {
|
|
525
|
-
return this.styleLastCommit(transitionState, 0, -28);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
protected styleLastCommitMovingDown(transitionState: 'start' | 'transitioning'): React.CSSProperties {
|
|
529
|
-
return this.styleLastCommit(transitionState, -28, 0);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
protected styleLastCommit(transitionState: 'start' | 'transitioning', startingMarginTop: number, startingMarginBottom: number): React.CSSProperties {
|
|
533
|
-
const base = {
|
|
534
|
-
display: 'flex',
|
|
535
|
-
width: '100%',
|
|
536
|
-
overflow: 'hidden',
|
|
537
|
-
paddingTop: 0,
|
|
538
|
-
paddingBottom: 0,
|
|
539
|
-
borderTop: 0,
|
|
540
|
-
borderBottom: 0,
|
|
541
|
-
height: this.lastCommitHeight * 2
|
|
542
|
-
};
|
|
543
|
-
|
|
544
|
-
// We end with top and bottom margins switched
|
|
545
|
-
const endingMarginTop = startingMarginBottom;
|
|
546
|
-
const endingMarginBottom = startingMarginTop;
|
|
547
|
-
|
|
548
|
-
switch (transitionState) {
|
|
549
|
-
case 'start':
|
|
550
|
-
return {
|
|
551
|
-
...base,
|
|
552
|
-
position: 'relative',
|
|
553
|
-
flexDirection: 'column',
|
|
554
|
-
marginTop: startingMarginTop,
|
|
555
|
-
marginBottom: startingMarginBottom,
|
|
556
|
-
};
|
|
557
|
-
case 'transitioning':
|
|
558
|
-
return {
|
|
559
|
-
...base,
|
|
560
|
-
position: 'relative',
|
|
561
|
-
flexDirection: 'column',
|
|
562
|
-
marginTop: endingMarginTop,
|
|
563
|
-
marginBottom: endingMarginBottom,
|
|
564
|
-
transitionProperty: 'margin-top margin-bottom',
|
|
565
|
-
transitionDuration: `${TRANSITION_TIME_MS}ms`,
|
|
566
|
-
transitionTimingFunction: 'linear'
|
|
567
|
-
};
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
readonly unamendAll = () => this.doUnamendAll();
|
|
572
|
-
protected async doUnamendAll(): Promise<void> {
|
|
573
|
-
while (this.state.amendingCommits.length > 0) {
|
|
574
|
-
this.unamend();
|
|
575
|
-
await new Promise(resolve => setTimeout(resolve, TRANSITION_TIME_MS));
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
readonly clearAmending = () => this.doClearAmending();
|
|
580
|
-
protected async doClearAmending(): Promise<void> {
|
|
581
|
-
await this.clearAmendingCommits();
|
|
582
|
-
this.setState({ amendingCommits: [] });
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
export namespace ScmAmendComponent {
|
|
587
|
-
|
|
588
|
-
export namespace Styles {
|
|
589
|
-
export const COMMIT_CONTAINER = 'theia-scm-commit-container';
|
|
590
|
-
export const COMMIT_AND_BUTTON = 'theia-scm-commit-and-button';
|
|
591
|
-
export const COMMIT_AVATAR_AND_TEXT = 'theia-scm-commit-avatar-and-text';
|
|
592
|
-
export const COMMIT_DETAILS = 'theia-scm-commit-details';
|
|
593
|
-
export const COMMIT_MESSAGE_AVATAR = 'theia-scm-commit-message-avatar';
|
|
594
|
-
export const COMMIT_MESSAGE_SUMMARY = 'theia-scm-commit-message-summary';
|
|
595
|
-
export const LAST_COMMIT_MESSAGE_TIME = 'theia-scm-commit-message-time';
|
|
596
|
-
|
|
597
|
-
export const FLEX_CENTER = 'theia-scm-flex-container-center';
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
}
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2019 Arm and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import '../../src/browser/style/scm-amend-component.css';
|
|
18
|
+
|
|
19
|
+
import * as React from '@theia/core/shared/react';
|
|
20
|
+
import { ScmAvatarService } from './scm-avatar-service';
|
|
21
|
+
import { codicon, StorageService } from '@theia/core/lib/browser';
|
|
22
|
+
import { Disposable, DisposableCollection } from '@theia/core';
|
|
23
|
+
|
|
24
|
+
import { ScmRepository } from './scm-repository';
|
|
25
|
+
import { ScmAmendSupport, ScmCommit } from './scm-provider';
|
|
26
|
+
import { nls } from '@theia/core/lib/common/nls';
|
|
27
|
+
|
|
28
|
+
export interface ScmAmendComponentProps {
|
|
29
|
+
style: React.CSSProperties | undefined,
|
|
30
|
+
repository: ScmRepository,
|
|
31
|
+
scmAmendSupport: ScmAmendSupport,
|
|
32
|
+
setCommitMessage: (message: string) => void,
|
|
33
|
+
avatarService: ScmAvatarService,
|
|
34
|
+
storageService: StorageService,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface ScmAmendComponentState {
|
|
38
|
+
/**
|
|
39
|
+
* This is used for transitioning. When setting up a transition, we first set to render
|
|
40
|
+
* the elements in their starting positions. This includes creating the elements to be
|
|
41
|
+
* transitioned in, even though those controls will not be visible when state is 'start'.
|
|
42
|
+
* On the next frame after 'start', we render elements with their final positions and with
|
|
43
|
+
* the transition properties.
|
|
44
|
+
*/
|
|
45
|
+
transition: {
|
|
46
|
+
state: 'none'
|
|
47
|
+
} | {
|
|
48
|
+
state: 'start' | 'transitioning',
|
|
49
|
+
direction: 'up' | 'down',
|
|
50
|
+
previousLastCommit: { commit: ScmCommit, avatar: string }
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
amendingCommits: { commit: ScmCommit, avatar: string }[];
|
|
54
|
+
lastCommit: { commit: ScmCommit, avatar: string } | undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const TRANSITION_TIME_MS = 300;
|
|
58
|
+
const REPOSITORY_STORAGE_KEY = 'scmRepository';
|
|
59
|
+
|
|
60
|
+
export class ScmAmendComponent extends React.Component<ScmAmendComponentProps, ScmAmendComponentState> {
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* a hint on how to animate an update, set by certain user action handlers
|
|
64
|
+
* and used when updating the view based on a repository change
|
|
65
|
+
*/
|
|
66
|
+
protected transitionHint: 'none' | 'amend' | 'unamend' = 'none';
|
|
67
|
+
|
|
68
|
+
protected lastCommitHeight: number = 0;
|
|
69
|
+
lastCommitScrollRef = (instance: HTMLDivElement) => {
|
|
70
|
+
if (instance && this.lastCommitHeight === 0) {
|
|
71
|
+
this.lastCommitHeight = instance.getBoundingClientRect().height;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
constructor(props: ScmAmendComponentProps) {
|
|
76
|
+
super(props);
|
|
77
|
+
|
|
78
|
+
this.state = {
|
|
79
|
+
transition: { state: 'none' },
|
|
80
|
+
amendingCommits: [],
|
|
81
|
+
lastCommit: undefined
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const setState = this.setState.bind(this);
|
|
85
|
+
this.setState = newState => {
|
|
86
|
+
if (!this.toDisposeOnUnmount.disposed) {
|
|
87
|
+
setState(newState);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
protected readonly toDisposeOnUnmount = new DisposableCollection();
|
|
93
|
+
|
|
94
|
+
override async componentDidMount(): Promise<void> {
|
|
95
|
+
this.toDisposeOnUnmount.push(Disposable.create(() => { /* mark as mounted */ }));
|
|
96
|
+
|
|
97
|
+
const lastCommit = await this.getLastCommit();
|
|
98
|
+
this.setState({ amendingCommits: await this.buildAmendingList(lastCommit ? lastCommit.commit : undefined), lastCommit });
|
|
99
|
+
|
|
100
|
+
if (this.toDisposeOnUnmount.disposed) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this.toDisposeOnUnmount.push(
|
|
104
|
+
this.props.repository.provider.onDidChange(() => this.fetchStatusAndSetState())
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
override componentWillUnmount(): void {
|
|
109
|
+
this.toDisposeOnUnmount.dispose();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async fetchStatusAndSetState(): Promise<void> {
|
|
113
|
+
const storageKey = this.getStorageKey();
|
|
114
|
+
|
|
115
|
+
const nextCommit = await this.getLastCommit();
|
|
116
|
+
if (nextCommit && this.state.lastCommit && nextCommit.commit.id === this.state.lastCommit.commit.id) {
|
|
117
|
+
// No change here
|
|
118
|
+
} else if (nextCommit === undefined && this.state.lastCommit === undefined) {
|
|
119
|
+
// No change here
|
|
120
|
+
} else if (this.transitionHint === 'none') {
|
|
121
|
+
// If the 'last' commit changes, but we are not expecting an 'amend'
|
|
122
|
+
// or 'unamend' to occur, then we clear out the list of amended commits.
|
|
123
|
+
// This is because an unexpected change has happened to the repository,
|
|
124
|
+
// perhaps the user committed, merged, or something. The amended commits
|
|
125
|
+
// will no longer be valid.
|
|
126
|
+
|
|
127
|
+
// Note that there may or may not have been a previous lastCommit (if the
|
|
128
|
+
// repository was previously empty with no initial commit then lastCommit
|
|
129
|
+
// will be undefined). Either way we clear the amending commits.
|
|
130
|
+
await this.clearAmendingCommits();
|
|
131
|
+
|
|
132
|
+
// There is a change to the last commit, but no transition hint so
|
|
133
|
+
// the view just updates without transition.
|
|
134
|
+
this.setState({ amendingCommits: [], lastCommit: nextCommit });
|
|
135
|
+
} else {
|
|
136
|
+
const amendingCommits = this.state.amendingCommits.concat([]); // copy the array
|
|
137
|
+
|
|
138
|
+
const direction: 'up' | 'down' = this.transitionHint === 'amend' ? 'up' : 'down';
|
|
139
|
+
switch (this.transitionHint) {
|
|
140
|
+
case 'amend':
|
|
141
|
+
if (this.state.lastCommit) {
|
|
142
|
+
amendingCommits.push(this.state.lastCommit);
|
|
143
|
+
|
|
144
|
+
const serializedState = JSON.stringify({
|
|
145
|
+
amendingHeadCommitSha: amendingCommits[0].commit.id,
|
|
146
|
+
latestCommitSha: nextCommit ? nextCommit.commit.id : undefined
|
|
147
|
+
});
|
|
148
|
+
this.props.storageService.setData<string | undefined>(storageKey, serializedState);
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
case 'unamend':
|
|
152
|
+
amendingCommits.pop();
|
|
153
|
+
if (amendingCommits.length === 0) {
|
|
154
|
+
this.props.storageService.setData<string | undefined>(storageKey, undefined);
|
|
155
|
+
} else {
|
|
156
|
+
const serializedState = JSON.stringify({
|
|
157
|
+
amendingHeadCommitSha: amendingCommits[0].commit.id,
|
|
158
|
+
latestCommitSha: nextCommit ? nextCommit.commit.id : undefined
|
|
159
|
+
});
|
|
160
|
+
this.props.storageService.setData<string | undefined>(storageKey, serializedState);
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (this.state.lastCommit && nextCommit) {
|
|
166
|
+
const transitionData = { direction, previousLastCommit: this.state.lastCommit };
|
|
167
|
+
this.setState({ lastCommit: nextCommit, amendingCommits, transition: { ...transitionData, state: 'start' } });
|
|
168
|
+
this.onNextFrame(() => {
|
|
169
|
+
this.setState({ transition: { ...transitionData, state: 'transitioning' } });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
setTimeout(
|
|
173
|
+
() => {
|
|
174
|
+
this.setState({ transition: { state: 'none' } });
|
|
175
|
+
},
|
|
176
|
+
TRANSITION_TIME_MS);
|
|
177
|
+
} else {
|
|
178
|
+
// No previous last commit so no transition
|
|
179
|
+
this.setState({ transition: { state: 'none' }, amendingCommits, lastCommit: nextCommit });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this.transitionHint = 'none';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private async clearAmendingCommits(): Promise<void> {
|
|
187
|
+
const storageKey = this.getStorageKey();
|
|
188
|
+
await this.props.storageService.setData<string | undefined>(storageKey, undefined);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private async buildAmendingList(lastCommit: ScmCommit | undefined): Promise<{ commit: ScmCommit, avatar: string }[]> {
|
|
192
|
+
const storageKey = this.getStorageKey();
|
|
193
|
+
const storedState = await this.props.storageService.getData<string | undefined>(storageKey, undefined);
|
|
194
|
+
|
|
195
|
+
// Restore list of commits from saved amending head commit up through parents until the
|
|
196
|
+
// current commit. (If we don't reach the current commit, the repository has been changed in such
|
|
197
|
+
// a way then unamending commits can no longer be done).
|
|
198
|
+
if (storedState) {
|
|
199
|
+
const { amendingHeadCommitSha, latestCommitSha } = JSON.parse(storedState);
|
|
200
|
+
if (!this.commitsAreEqual(lastCommit, latestCommitSha)) {
|
|
201
|
+
// The head commit in the repository has changed. It is not the same commit that was the
|
|
202
|
+
// head commit after the last 'amend'.
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
const commits = await this.props.scmAmendSupport.getInitialAmendingCommits(amendingHeadCommitSha, lastCommit ? lastCommit.id : undefined);
|
|
206
|
+
|
|
207
|
+
const amendingCommitPromises = commits.map(async commit => {
|
|
208
|
+
const avatar = await this.props.avatarService.getAvatar(commit.authorEmail);
|
|
209
|
+
return { commit, avatar };
|
|
210
|
+
});
|
|
211
|
+
return Promise.all(amendingCommitPromises);
|
|
212
|
+
} else {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private getStorageKey(): string {
|
|
218
|
+
return REPOSITORY_STORAGE_KEY + ':' + this.props.repository.provider.rootUri;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Commits are equal if the ids are equal or if both are undefined.
|
|
223
|
+
* (If a commit is undefined, it represents the initial empty state of a repository,
|
|
224
|
+
* before the initial commit).
|
|
225
|
+
*/
|
|
226
|
+
private commitsAreEqual(lastCommit: ScmCommit | undefined, savedLastCommitId: string | undefined): boolean {
|
|
227
|
+
return lastCommit
|
|
228
|
+
? lastCommit.id === savedLastCommitId
|
|
229
|
+
: savedLastCommitId === undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* This function will update the 'model' (lastCommit, amendingCommits) only
|
|
234
|
+
* when the repository sees the last commit change.
|
|
235
|
+
* 'render' can be called at any time, so be sure we don't update any 'model'
|
|
236
|
+
* fields until we actually start the transition.
|
|
237
|
+
*/
|
|
238
|
+
protected amend = async (): Promise<void> => {
|
|
239
|
+
if (this.state.transition.state !== 'none' && this.transitionHint !== 'none') {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
this.transitionHint = 'amend';
|
|
244
|
+
await this.resetAndSetMessage('HEAD~', 'HEAD');
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
protected unamend = async (): Promise<void> => {
|
|
248
|
+
if (this.state.transition.state !== 'none' && this.transitionHint !== 'none') {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const commitToRestore = (this.state.amendingCommits.length >= 1)
|
|
253
|
+
? this.state.amendingCommits[this.state.amendingCommits.length - 1]
|
|
254
|
+
: undefined;
|
|
255
|
+
const oldestAmendCommit = (this.state.amendingCommits.length >= 2)
|
|
256
|
+
? this.state.amendingCommits[this.state.amendingCommits.length - 2]
|
|
257
|
+
: undefined;
|
|
258
|
+
|
|
259
|
+
if (commitToRestore) {
|
|
260
|
+
const commitToUseForMessage = oldestAmendCommit
|
|
261
|
+
? oldestAmendCommit.commit.id
|
|
262
|
+
: undefined;
|
|
263
|
+
this.transitionHint = 'unamend';
|
|
264
|
+
await this.resetAndSetMessage(commitToRestore.commit.id, commitToUseForMessage);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
private async resetAndSetMessage(commitToRestore: string, commitToUseForMessage: string | undefined): Promise<void> {
|
|
269
|
+
const message = commitToUseForMessage
|
|
270
|
+
? await this.props.scmAmendSupport.getMessage(commitToUseForMessage)
|
|
271
|
+
: '';
|
|
272
|
+
await this.props.scmAmendSupport.reset(commitToRestore);
|
|
273
|
+
this.props.setCommitMessage(message);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
override render(): JSX.Element {
|
|
277
|
+
const neverShrink = this.state.amendingCommits.length <= 3;
|
|
278
|
+
|
|
279
|
+
const style: React.CSSProperties = neverShrink
|
|
280
|
+
? {
|
|
281
|
+
...this.props.style,
|
|
282
|
+
flexShrink: 0,
|
|
283
|
+
}
|
|
284
|
+
: {
|
|
285
|
+
...this.props.style,
|
|
286
|
+
flexShrink: 1,
|
|
287
|
+
minHeight: 240 // height with three commits
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<div className={ScmAmendComponent.Styles.COMMIT_CONTAINER + ' no-select'} style={style}>
|
|
292
|
+
{
|
|
293
|
+
this.state.amendingCommits.length > 0 || (this.state.lastCommit && this.state.transition.state !== 'none' && this.state.transition.direction === 'down')
|
|
294
|
+
? this.renderAmendingCommits()
|
|
295
|
+
: ''
|
|
296
|
+
}
|
|
297
|
+
{
|
|
298
|
+
this.state.lastCommit ?
|
|
299
|
+
<div>
|
|
300
|
+
<div id='lastCommit' className='theia-scm-amend'>
|
|
301
|
+
<div className='theia-header scm-theia-header'>
|
|
302
|
+
{nls.localize('theia/scm/amendHeadCommit', 'HEAD Commit')}
|
|
303
|
+
</div>
|
|
304
|
+
{this.renderLastCommit()}
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
: ''
|
|
308
|
+
}
|
|
309
|
+
</div>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
protected async getLastCommit(): Promise<{ commit: ScmCommit, avatar: string } | undefined> {
|
|
314
|
+
const commit = await this.props.scmAmendSupport.getLastCommit();
|
|
315
|
+
if (commit) {
|
|
316
|
+
const avatar = await this.props.avatarService.getAvatar(commit.authorEmail);
|
|
317
|
+
return { commit, avatar };
|
|
318
|
+
}
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
protected renderAmendingCommits(): React.ReactNode {
|
|
323
|
+
const neverShrink = this.state.amendingCommits.length <= 3;
|
|
324
|
+
|
|
325
|
+
const style: React.CSSProperties = neverShrink
|
|
326
|
+
? {
|
|
327
|
+
flexShrink: 0,
|
|
328
|
+
}
|
|
329
|
+
: {
|
|
330
|
+
flexShrink: 1,
|
|
331
|
+
// parent minHeight controls height, we just need any value smaller than
|
|
332
|
+
// what the height would be when the parent is at its minHeight
|
|
333
|
+
minHeight: 0
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
return <div id='amendedCommits' className='theia-scm-amend-outer-container' style={style}>
|
|
337
|
+
<div className='theia-header scm-theia-header'>
|
|
338
|
+
<div className='noWrapInfo'>Commits being Amended</div>
|
|
339
|
+
{this.renderAmendCommitListButtons()}
|
|
340
|
+
{this.renderCommitCount(this.state.amendingCommits.length)}
|
|
341
|
+
</div>
|
|
342
|
+
<div style={this.styleAmendedCommits()}>
|
|
343
|
+
{this.state.amendingCommits.map((commitData, index, array) =>
|
|
344
|
+
this.renderCommitBeingAmended(commitData, index === array.length - 1)
|
|
345
|
+
)}
|
|
346
|
+
{
|
|
347
|
+
this.state.lastCommit && this.state.transition.state !== 'none' && this.state.transition.direction === 'down'
|
|
348
|
+
? this.renderCommitBeingAmended(this.state.lastCommit, false)
|
|
349
|
+
: ''
|
|
350
|
+
}
|
|
351
|
+
</div>
|
|
352
|
+
</div>;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
protected renderAmendCommitListButtons(): React.ReactNode {
|
|
356
|
+
return <div className='theia-scm-inline-actions-container'>
|
|
357
|
+
<div className='theia-scm-inline-actions'>
|
|
358
|
+
<div className='theia-scm-inline-action'>
|
|
359
|
+
<a className={codicon('dash')} title='Unamend All Commits' onClick={this.unamendAll} />
|
|
360
|
+
</div>
|
|
361
|
+
<div className='theia-scm-inline-action' >
|
|
362
|
+
<a className={codicon('close')} title='Clear Amending Commits' onClick={this.clearAmending} />
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
</div>;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
protected renderLastCommit(): React.ReactNode {
|
|
369
|
+
if (!this.state.lastCommit) {
|
|
370
|
+
return '';
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const canAmend: boolean = true;
|
|
374
|
+
return <div className={ScmAmendComponent.Styles.COMMIT_AND_BUTTON} style={{ flexGrow: 0, flexShrink: 0 }} key={this.state.lastCommit.commit.id}>
|
|
375
|
+
{this.renderLastCommitNoButton(this.state.lastCommit)}
|
|
376
|
+
{
|
|
377
|
+
canAmend
|
|
378
|
+
? <div className={ScmAmendComponent.Styles.FLEX_CENTER}>
|
|
379
|
+
<button className='theia-button' title={nls.localize('theia/scm/amendLastCommit', 'Amend last commit')} onClick={this.amend}>
|
|
380
|
+
{nls.localize('theia/scm/amend', 'Amend')}
|
|
381
|
+
</button>
|
|
382
|
+
</div>
|
|
383
|
+
: ''
|
|
384
|
+
}
|
|
385
|
+
</div>;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
protected renderLastCommitNoButton(lastCommit: { commit: ScmCommit, avatar: string }): React.ReactNode {
|
|
389
|
+
switch (this.state.transition.state) {
|
|
390
|
+
case 'none':
|
|
391
|
+
return <div ref={this.lastCommitScrollRef} className='theia-scm-scrolling-container'>
|
|
392
|
+
{this.renderCommitAvatarAndDetail(lastCommit)}
|
|
393
|
+
</div>;
|
|
394
|
+
|
|
395
|
+
case 'start':
|
|
396
|
+
case 'transitioning':
|
|
397
|
+
switch (this.state.transition.direction) {
|
|
398
|
+
case 'up':
|
|
399
|
+
return <div style={this.styleLastCommitMovingUp(this.state.transition.state)}>
|
|
400
|
+
{this.renderCommitAvatarAndDetail(this.state.transition.previousLastCommit)}
|
|
401
|
+
{this.renderCommitAvatarAndDetail(lastCommit)}
|
|
402
|
+
</div>;
|
|
403
|
+
case 'down':
|
|
404
|
+
return <div style={this.styleLastCommitMovingDown(this.state.transition.state)}>
|
|
405
|
+
{this.renderCommitAvatarAndDetail(lastCommit)}
|
|
406
|
+
{this.renderCommitAvatarAndDetail(this.state.transition.previousLastCommit)}
|
|
407
|
+
</div>;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* See https://stackoverflow.com/questions/26556436/react-after-render-code
|
|
414
|
+
*
|
|
415
|
+
* @param callback
|
|
416
|
+
*/
|
|
417
|
+
protected onNextFrame(callback: FrameRequestCallback): void {
|
|
418
|
+
setTimeout(
|
|
419
|
+
() => window.requestAnimationFrame(callback),
|
|
420
|
+
0);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
protected renderCommitAvatarAndDetail(commitData: { commit: ScmCommit, avatar: string }): React.ReactNode {
|
|
424
|
+
const { commit, avatar } = commitData;
|
|
425
|
+
return <div className={ScmAmendComponent.Styles.COMMIT_AVATAR_AND_TEXT} key={commit.id}>
|
|
426
|
+
<div className={ScmAmendComponent.Styles.COMMIT_MESSAGE_AVATAR}>
|
|
427
|
+
<img src={avatar} />
|
|
428
|
+
</div>
|
|
429
|
+
<div className={ScmAmendComponent.Styles.COMMIT_DETAILS}>
|
|
430
|
+
<div className={ScmAmendComponent.Styles.COMMIT_MESSAGE_SUMMARY}>{commit.summary}</div>
|
|
431
|
+
<div className={ScmAmendComponent.Styles.LAST_COMMIT_MESSAGE_TIME}>{`${commit.authorDateRelative} by ${commit.authorName}`}</div>
|
|
432
|
+
</div>
|
|
433
|
+
</div>;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
protected renderCommitCount(commits: number): React.ReactNode {
|
|
437
|
+
return <div className='notification-count-container scm-change-count'>
|
|
438
|
+
<span className='notification-count'>{commits}</span>
|
|
439
|
+
</div>;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
protected renderCommitBeingAmended(commitData: { commit: ScmCommit, avatar: string }, isOldestAmendCommit: boolean): JSX.Element {
|
|
443
|
+
if (isOldestAmendCommit && this.state.transition.state !== 'none' && this.state.transition.direction === 'up') {
|
|
444
|
+
return <div className={ScmAmendComponent.Styles.COMMIT_AVATAR_AND_TEXT} style={{ flexGrow: 0, flexShrink: 0 }} key={commitData.commit.id}>
|
|
445
|
+
<div className='fixed-height-commit-container'>
|
|
446
|
+
{this.renderCommitAvatarAndDetail(commitData)}
|
|
447
|
+
</div>
|
|
448
|
+
</div>;
|
|
449
|
+
} else {
|
|
450
|
+
return <div className={ScmAmendComponent.Styles.COMMIT_AVATAR_AND_TEXT} style={{ flexGrow: 0, flexShrink: 0 }} key={commitData.commit.id}>
|
|
451
|
+
{this.renderCommitAvatarAndDetail(commitData)}
|
|
452
|
+
{
|
|
453
|
+
isOldestAmendCommit
|
|
454
|
+
? <div className={ScmAmendComponent.Styles.FLEX_CENTER}>
|
|
455
|
+
<button className='theia-button' title={nls.localize('theia/scm/unamendCommit', 'Unamend commit')} onClick={this.unamend}>
|
|
456
|
+
{nls.localize('theia/scm/unamend', 'Unamend')}
|
|
457
|
+
</button>
|
|
458
|
+
</div>
|
|
459
|
+
: ''
|
|
460
|
+
}
|
|
461
|
+
</div>;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/*
|
|
466
|
+
* The style for the <div> containing the list of commits being amended.
|
|
467
|
+
* This div is scrollable.
|
|
468
|
+
*/
|
|
469
|
+
protected styleAmendedCommits(): React.CSSProperties {
|
|
470
|
+
const base = {
|
|
471
|
+
display: 'flex',
|
|
472
|
+
whitespace: 'nowrap',
|
|
473
|
+
width: '100%',
|
|
474
|
+
minHeight: 0,
|
|
475
|
+
flexShrink: 1,
|
|
476
|
+
paddingTop: '2px',
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
switch (this.state.transition.state) {
|
|
480
|
+
case 'none':
|
|
481
|
+
return {
|
|
482
|
+
...base,
|
|
483
|
+
flexDirection: 'column',
|
|
484
|
+
overflowY: 'auto',
|
|
485
|
+
marginBottom: '0',
|
|
486
|
+
};
|
|
487
|
+
case 'start':
|
|
488
|
+
case 'transitioning':
|
|
489
|
+
let startingMargin: number = 0;
|
|
490
|
+
let endingMargin: number = 0;
|
|
491
|
+
switch (this.state.transition.direction) {
|
|
492
|
+
case 'down':
|
|
493
|
+
startingMargin = 0;
|
|
494
|
+
endingMargin = -32;
|
|
495
|
+
break;
|
|
496
|
+
case 'up':
|
|
497
|
+
startingMargin = -32;
|
|
498
|
+
endingMargin = 0;
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
switch (this.state.transition.state) {
|
|
503
|
+
case 'start':
|
|
504
|
+
return {
|
|
505
|
+
...base,
|
|
506
|
+
flexDirection: 'column',
|
|
507
|
+
overflowY: 'hidden',
|
|
508
|
+
marginBottom: `${startingMargin}px`,
|
|
509
|
+
};
|
|
510
|
+
case 'transitioning':
|
|
511
|
+
return {
|
|
512
|
+
...base,
|
|
513
|
+
flexDirection: 'column',
|
|
514
|
+
overflowY: 'hidden',
|
|
515
|
+
marginBottom: `${endingMargin}px`,
|
|
516
|
+
transitionProperty: 'margin-bottom',
|
|
517
|
+
transitionDuration: `${TRANSITION_TIME_MS}ms`,
|
|
518
|
+
transitionTimingFunction: 'linear'
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
protected styleLastCommitMovingUp(transitionState: 'start' | 'transitioning'): React.CSSProperties {
|
|
525
|
+
return this.styleLastCommit(transitionState, 0, -28);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
protected styleLastCommitMovingDown(transitionState: 'start' | 'transitioning'): React.CSSProperties {
|
|
529
|
+
return this.styleLastCommit(transitionState, -28, 0);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
protected styleLastCommit(transitionState: 'start' | 'transitioning', startingMarginTop: number, startingMarginBottom: number): React.CSSProperties {
|
|
533
|
+
const base = {
|
|
534
|
+
display: 'flex',
|
|
535
|
+
width: '100%',
|
|
536
|
+
overflow: 'hidden',
|
|
537
|
+
paddingTop: 0,
|
|
538
|
+
paddingBottom: 0,
|
|
539
|
+
borderTop: 0,
|
|
540
|
+
borderBottom: 0,
|
|
541
|
+
height: this.lastCommitHeight * 2
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// We end with top and bottom margins switched
|
|
545
|
+
const endingMarginTop = startingMarginBottom;
|
|
546
|
+
const endingMarginBottom = startingMarginTop;
|
|
547
|
+
|
|
548
|
+
switch (transitionState) {
|
|
549
|
+
case 'start':
|
|
550
|
+
return {
|
|
551
|
+
...base,
|
|
552
|
+
position: 'relative',
|
|
553
|
+
flexDirection: 'column',
|
|
554
|
+
marginTop: startingMarginTop,
|
|
555
|
+
marginBottom: startingMarginBottom,
|
|
556
|
+
};
|
|
557
|
+
case 'transitioning':
|
|
558
|
+
return {
|
|
559
|
+
...base,
|
|
560
|
+
position: 'relative',
|
|
561
|
+
flexDirection: 'column',
|
|
562
|
+
marginTop: endingMarginTop,
|
|
563
|
+
marginBottom: endingMarginBottom,
|
|
564
|
+
transitionProperty: 'margin-top margin-bottom',
|
|
565
|
+
transitionDuration: `${TRANSITION_TIME_MS}ms`,
|
|
566
|
+
transitionTimingFunction: 'linear'
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
readonly unamendAll = () => this.doUnamendAll();
|
|
572
|
+
protected async doUnamendAll(): Promise<void> {
|
|
573
|
+
while (this.state.amendingCommits.length > 0) {
|
|
574
|
+
this.unamend();
|
|
575
|
+
await new Promise(resolve => setTimeout(resolve, TRANSITION_TIME_MS));
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
readonly clearAmending = () => this.doClearAmending();
|
|
580
|
+
protected async doClearAmending(): Promise<void> {
|
|
581
|
+
await this.clearAmendingCommits();
|
|
582
|
+
this.setState({ amendingCommits: [] });
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export namespace ScmAmendComponent {
|
|
587
|
+
|
|
588
|
+
export namespace Styles {
|
|
589
|
+
export const COMMIT_CONTAINER = 'theia-scm-commit-container';
|
|
590
|
+
export const COMMIT_AND_BUTTON = 'theia-scm-commit-and-button';
|
|
591
|
+
export const COMMIT_AVATAR_AND_TEXT = 'theia-scm-commit-avatar-and-text';
|
|
592
|
+
export const COMMIT_DETAILS = 'theia-scm-commit-details';
|
|
593
|
+
export const COMMIT_MESSAGE_AVATAR = 'theia-scm-commit-message-avatar';
|
|
594
|
+
export const COMMIT_MESSAGE_SUMMARY = 'theia-scm-commit-message-summary';
|
|
595
|
+
export const LAST_COMMIT_MESSAGE_TIME = 'theia-scm-commit-message-time';
|
|
596
|
+
|
|
597
|
+
export const FLEX_CENTER = 'theia-scm-flex-container-center';
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
}
|