-
Notifications
You must be signed in to change notification settings - Fork 1
/
extension.ts
317 lines (270 loc) · 11.6 KB
/
extension.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from 'vscode';
// import * as path from 'path';
import * as git from 'simple-git';
import * as branchInfo from './branchinfo';
import { randomUUID } from 'crypto';
// This method is called when your extension is activated
// Your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {
var extension = new GitBackupSync(context);
vscode.workspace.onDidChangeConfiguration(() => {
extension.loadConfig();
});
vscode.workspace.onDidSaveTextDocument(async (document: vscode.TextDocument) => {
// TODO: do we want to ignore document if it is the branchinfo file?
let branchName = await extension.getCurrentBranchName();
let shouldBackup = await extension.shouldAutoBackup(branchName);
if (shouldBackup) {
await extension.backup(branchName);
}
});
// Use the console to output diagnostic information (console.log) and errors (console.error)
// This line of code will only be executed once when your extension is activated
extension.showOutputMessage('Congratulations, your extension "git-backup-sync" is now active!');
context.subscriptions.push(
vscode.commands.registerCommand('git-backup-sync.createBackupBranch',
() => extension.createBackupBranch()
)
);
context.subscriptions.push(
vscode.commands.registerCommand('git-backup-sync.retireBackupBranch',
() => extension.retireBackupBranch()
)
);
context.subscriptions.push(
vscode.commands.registerCommand('git-backup-sync.syncBackupBranch',
() => extension.syncBackupBranch()
)
);
context.subscriptions.push(
vscode.commands.registerCommand('git-backup-sync.backup',
() => extension.backup()
)
);
context.subscriptions.push(
vscode.commands.registerCommand('git-backup-sync.loadBackup',
() => extension.loadBackup()
)
);
}
// This method is called when your extension is deactivated
export function deactivate() { }
interface IConfig {
branchInfoPath: string;
defaultBackupUpstreamName: string;
defaultAutoBackupBranches: boolean;
shouldCommitBranchInfoFile: boolean;
}
class GitBackupSync {
private _outputChannel: vscode.OutputChannel;
private _context: vscode.ExtensionContext;
private config: vscode.WorkspaceConfiguration;
private _config: IConfig;
private _git: git.SimpleGit;
private _branchInfo: branchInfo.BranchInfoManager;
constructor(context: vscode.ExtensionContext) {
this._context = context;
this._outputChannel = vscode.window.createOutputChannel('Git Backup & Sync');
this.config = vscode.workspace.getConfiguration('git-backup-sync');
this._config = <IConfig><any>this.config;
console.log(this._config);
if (vscode.workspace.workspaceFolders !== undefined) {
console.log(vscode.workspace.workspaceFolders[0].uri.fsPath);
this._git = git.simpleGit({ baseDir: vscode.workspace.workspaceFolders[0].uri.fsPath });
this._branchInfo = new branchInfo.BranchInfoManager(
vscode.workspace.workspaceFolders[0].uri
);
} else {
throw new Error("Git Backup & Sync: Failed to activate extension, this can only be ran from within a workspace.");
}
}
public get isEnabled(): boolean {
return !!this._context.globalState.get('isEnabled', true);
}
public set isEnabled(value: boolean) {
this._context.globalState.update('isEnabled', value);
}
public loadConfig(): void {
this.config = vscode.workspace.getConfiguration('git-backup-sync');
this._config = <IConfig><any>this.config;
}
/**
* Show message in output channel
*/
public showOutputMessage(message: string): void {
console.log(message);
this._outputChannel.appendLine(message);
}
/**
* Show message in status bar and output channel.
* Return a disposable to remove status bar message.
*/
public async showInformationMessage(message: string) {
this.showOutputMessage(message);
return await vscode.window.showInformationMessage(message);
}
public async showErrorMessage(message: string) {
this.showOutputMessage(message);
return await vscode.window.showErrorMessage(message);
}
public async getCurrentBranchName(branchName?: string): Promise<string> {
this.showOutputMessage("Getting current branch name");
if (branchName !== undefined) {
return branchName;
}
return this._git.branch().then(value => {
this.showOutputMessage("git current branch is: " + value.current);
return value.current;
});
}
public async shouldAutoBackup(branchName?: string): Promise<boolean> {
if (!this.isEnabled) {
return false;
}
return this.getCurrentBranchName(branchName).then(currentBranchName =>
this._branchInfo.get(this._config.branchInfoPath, currentBranchName).then(v => {
if (v === undefined) {
return false;
}
return v.autoBackup;
})
);
}
public async createBackupBranch(branchName?: string): Promise<string | undefined> {
if (!this.isEnabled) {
return;
}
let currentBranchName: string = await this.getCurrentBranchName(branchName);
this.showOutputMessage(`Creating Backup Branch for "${currentBranchName}"`);
let branchInfoMap = await this._branchInfo.getMap(this._config.branchInfoPath);
if (branchInfoMap.has(currentBranchName)) {
this.showErrorMessage(`Create Backup Branch failed: "${currentBranchName}" already has a backup branch, aborting`);
return;
}
let backupBranchNames = new Set(Array.from(branchInfoMap.values()).map(info => info.backupBranchName));
let backupBranchName = await vscode.window.showInputBox({
placeHolder: "Type a Branch Name, or press ENTER and use the default",
prompt: "Create Backup Branch with Name"
});
if (backupBranchName === undefined) {
this.showInformationMessage("Create Backup Branch cancelled");
return;
}
backupBranchName = (backupBranchName !== "") ? backupBranchName : "gbs-backup-" + currentBranchName;
if (backupBranchNames.has(backupBranchName)) {
this.showErrorMessage(`Create Backup Branch failed: intended backup branch name "${backupBranchName}" already exists`);
return;
}
if (branchInfoMap.has(currentBranchName)) {
this.showErrorMessage(`Create Backup Branch failed: "${currentBranchName}" already has a backup branch, aborting`);
return;
}
await this._branchInfo.update(this._config.branchInfoPath, currentBranchName, {
autoBackup: this._config.defaultAutoBackupBranches,
backupBranchName: backupBranchName,
});
// unstage current changes
console.log(await this._git.reset());
if (this._config.shouldCommitBranchInfoFile) {
console.log(await this._git.add(this._config.branchInfoPath));
console.log(await this._git.commit(`git-backup-sync: create backup branch for [${currentBranchName}]`));
}
console.log(await this._git.branch([backupBranchName, currentBranchName]));
return backupBranchName;
}
public async retireBackupBranch(branchName?: string): Promise<void> {
if (!this.isEnabled) {
return;
}
let currentBranchName: string = await this.getCurrentBranchName(branchName);
this.showOutputMessage(`Retiring Backup Branch for "${currentBranchName}"`);
let branchInfoMap = await this._branchInfo.getMap(this._config.branchInfoPath);
let backupBranchInfo = branchInfoMap.get(currentBranchName);
if (backupBranchInfo === undefined) {
this.showErrorMessage(`Retire Backup Branch Aborted: "${currentBranchName}" doesn't have a backup branch, aborting`);
return;
}
console.log(await this._branchInfo.delete(this._config.branchInfoPath, currentBranchName));
console.log(await this._git.deleteLocalBranch(backupBranchInfo.backupBranchName));
// TODO: undo the "git-backup-sync: create backup branch" commit?
}
public async syncBackupBranch(branchName?: string): Promise<string | undefined> {
if (!this.isEnabled) {
return;
}
let currentBranchName: string = await this.getCurrentBranchName(branchName);
this.showOutputMessage(`Syncing Backup Branch for "${currentBranchName}"`);
let branchInfoMap = await this._branchInfo.getMap(this._config.branchInfoPath);
let backupBranchInfo = branchInfoMap.get(currentBranchName);
if (backupBranchInfo === undefined) {
this.showErrorMessage(`Sync Backup Branch Aborted: "${currentBranchName}" doesn't have a backup branch, aborting`);
return;
}
// by recreating the local backup branch, we make sure the local backup branch is in sync to the local branch
console.log(await this._git.deleteLocalBranch(backupBranchInfo.backupBranchName));
console.log(await this._git.branch([backupBranchInfo.backupBranchName, currentBranchName]));
}
public async backup(branchName?: string): Promise<void> {
if (!this.isEnabled) {
return;
}
let currentBranchName = await this.getCurrentBranchName(branchName);
this.showOutputMessage(`Backing up Current Branch "${currentBranchName}"`);
let branchInfo = await this._branchInfo.get(this._config.branchInfoPath, currentBranchName);
if (branchInfo === undefined) {
this.showErrorMessage("Backup failed: no backup branch found. Please create backup branch first");
return;
}
let backupBranchName: string = branchInfo.backupBranchName;
// unstages current changes
console.log(await this._git.reset());
try {
console.log(await this._git.checkout(backupBranchName));
} catch(exception) {
console.log(exception);
// TODO: might want to add an auto refresh backup branch
this.showErrorMessage("Backup failed during checkout backup branch, backup branch is likely out of sync with your branch "
+ currentBranchName + ". Did you refresh your backup branch after last commit, or forgot to pull your current branch? ");
return;
}
// stage and commits current changes to backup branch
console.log(await this._git.add("."));
console.log(await this._git.commit(`git-backup-sync: backup commit [${randomUUID()}]`));
// force pushes since we are rewriting git history
console.log(await this._git.push(this._config.defaultBackupUpstreamName, backupBranchName, ["--force"]));
// IMPORTANT: the local backup branch is ALWAYS maintained in a state in sync with your local branch
// It is therefore always lagging behind the upstream backup branch.
console.log(await this._git.reset(["--mixed", "HEAD~1"]));
console.log(await this._git.checkout(currentBranchName));
}
public async loadBackup(branchName?: string): Promise<boolean> {
if (!this.isEnabled) {
return false;
}
let currentBranchName = await this.getCurrentBranchName(branchName);
this.showOutputMessage(`Loading Backup for "${currentBranchName}"`);
if (vscode.workspace.textDocuments.filter(document => document.isDirty).length !== 0) {
this.showErrorMessage("Load Backup aborted: Detected unsaved changes in your current workspace");
return false;
}
let branchInfo = await this._branchInfo.get(this._config.branchInfoPath, currentBranchName);
if (branchInfo === undefined) {
this.showErrorMessage(`Load Backup failed: No backup branch found for "${currentBranchName}"`);
return false;
}
let backupBranchName: string = branchInfo.backupBranchName;
// stashes the current changes. Normally this shouldn't do anything since you wouldn't want to load backup with local changes
console.log(await this._git.stash());
console.log(await this._git.checkout(backupBranchName));
console.log(await this._git.fetch());
// pulls in the most up to date backup branch
console.log(await this._git.reset(["--hard", this._config.defaultBackupUpstreamName + "/" + backupBranchName]));
// undoes the last commit
console.log(await this._git.reset(["--mixed", "HEAD~1"]));
// brings those changes in the last commit over to the current branch
console.log(await this._git.checkout(currentBranchName));
return true;
}
}