Skip to content

Commit 46597b8

Browse files
committed
Add pull feature with conflict detection and auto-pull on open
1 parent 9812899 commit 46597b8

File tree

6 files changed

+236
-14
lines changed

6 files changed

+236
-14
lines changed

main.js

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/git.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,52 @@ export async function commitAll(cwd: string, gitPath: string, message: string):
6565
}
6666
}
6767

68+
async function getCurrentBranch(cwd: string, gitPath: string): Promise<string> {
69+
const stdout = await runGit({ cwd, gitPath, args: ["rev-parse", "--abbrev-ref", "HEAD"] });
70+
return stdout.trim();
71+
}
72+
6873
export async function push(cwd: string, gitPath: string): Promise<void> {
69-
await runGit({ cwd, gitPath, args: ["push"] });
74+
const branch = await getCurrentBranch(cwd, gitPath);
75+
await runGit({ cwd, gitPath, args: ["push", "-u", "origin", branch] });
76+
}
77+
78+
export interface PullResult {
79+
success: boolean;
80+
hasConflicts: boolean;
81+
message: string;
82+
}
83+
84+
export async function pull(cwd: string, gitPath: string): Promise<PullResult> {
85+
try {
86+
const branch = await getCurrentBranch(cwd, gitPath);
87+
const stdout = await runGit({ cwd, gitPath, args: ["pull", "origin", branch] });
88+
return { success: true, hasConflicts: false, message: stdout };
89+
} catch (e) {
90+
const msg = (e as Error).message;
91+
if (msg.includes("CONFLICT") || msg.includes("Merge conflict")) {
92+
return { success: false, hasConflicts: true, message: msg };
93+
}
94+
throw e;
95+
}
96+
}
97+
98+
export async function getConflictFiles(cwd: string, gitPath: string): Promise<string[]> {
99+
try {
100+
const stdout = await runGit({ cwd, gitPath, args: ["diff", "--name-only", "--diff-filter=U"] });
101+
return stdout.split("\n").map(f => f.trim()).filter(Boolean);
102+
} catch {
103+
return [];
104+
}
105+
}
106+
107+
export async function hasConflicts(cwd: string, gitPath: string): Promise<boolean> {
108+
const files = await getConflictFiles(cwd, gitPath);
109+
return files.length > 0;
110+
}
111+
112+
export async function markConflictsResolved(cwd: string, gitPath: string): Promise<void> {
113+
await runGit({ cwd, gitPath, args: ["add", "-A"] });
70114
}
71115

72116
export async function isGitRepo(cwd: string, gitPath: string): Promise<boolean> {
@@ -100,7 +144,7 @@ export async function setRemoteUrl(cwd: string, gitPath: string, url: string): P
100144
}
101145
}
102146

103-
export type FileStatus = "M" | "A" | "D" | "R" | "?" | "";
147+
export type FileStatus = "M" | "A" | "D" | "R" | "U" | "?" | "";
104148

105149
export async function getFileStatuses(cwd: string, gitPath: string): Promise<Map<string, FileStatus>> {
106150
const statusMap = new Map<string, FileStatus>();

src/i18n.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ type Translations = {
1414
autoPushName: string;
1515
autoPushDesc: string;
1616

17+
autoPullOnOpenName: string;
18+
autoPullOnOpenDesc: string;
19+
1720
templateName: string;
1821
templateDesc: string;
1922

@@ -39,16 +42,26 @@ type Translations = {
3942
remoteUrlPlaceholder: string;
4043
saveButton: string;
4144

45+
// Conflict
46+
conflictStatusName: string;
47+
conflictStatusDesc: string;
48+
resolveConflictButton: string;
49+
4250
// Notices
4351
noticeNoChanges: string;
4452
noticeCommitted: (count: number) => string;
4553
noticePushed: string;
54+
noticePulled: string;
4655
noticeAutoGitError: (msg: string) => string;
4756
noticePushFailed: (msg: string) => string;
57+
noticePullFailed: (msg: string) => string;
4858
noticeMobileNotSupported: string;
4959
noticeDesktopOnly: string;
5060
noticeRepoInitialized: string;
5161
noticeRemoteSaved: string;
62+
noticeConflictDetected: string;
63+
noticeConflictResolved: string;
64+
noticeCannotCommitConflict: string;
5265
};
5366

5467
const en: Translations = {
@@ -66,6 +79,9 @@ const en: Translations = {
6679
autoPushName: "Auto push after commit",
6780
autoPushDesc: "Push to remote after successful commit.",
6881

82+
autoPullOnOpenName: "Auto pull on open",
83+
autoPullOnOpenDesc: "Pull from remote when Obsidian opens.",
84+
6985
templateName: "Commit message template",
7086
templateDesc: "Variables: {{date}}, {{time}}, {{files}}, {{count}}",
7187

@@ -90,15 +106,24 @@ const en: Translations = {
90106
remoteUrlPlaceholder: "https://github.com/user/repo.git",
91107
saveButton: "Save",
92108

109+
conflictStatusName: "Merge conflicts detected",
110+
conflictStatusDesc: "Please resolve conflicts manually, then click the button below.",
111+
resolveConflictButton: "Mark as resolved",
112+
93113
noticeNoChanges: "GitAutoCommit: No changes to commit.",
94114
noticeCommitted: (count) => `GitAutoCommit: Committed ${count} file(s).`,
95115
noticePushed: "GitAutoCommit: Pushed to remote.",
116+
noticePulled: "GitAutoCommit: Pulled from remote.",
96117
noticeAutoGitError: (msg) => `GitAutoCommit: Error - ${msg}`,
97118
noticePushFailed: (msg) => `GitAutoCommit: Push failed - ${msg}`,
119+
noticePullFailed: (msg) => `GitAutoCommit: Pull failed - ${msg}`,
98120
noticeMobileNotSupported: "GitAutoCommit: Git not available on mobile.",
99121
noticeDesktopOnly: "GitAutoCommit: Requires desktop vault.",
100122
noticeRepoInitialized: "GitAutoCommit: Repository initialized.",
101123
noticeRemoteSaved: "GitAutoCommit: Remote URL saved.",
124+
noticeConflictDetected: "GitAutoCommit: Merge conflicts detected! Please resolve manually.",
125+
noticeConflictResolved: "GitAutoCommit: Conflicts marked as resolved.",
126+
noticeCannotCommitConflict: "GitAutoCommit: Cannot commit while conflicts exist.",
102127
};
103128

104129
const zhCN: Translations = {
@@ -116,6 +141,9 @@ const zhCN: Translations = {
116141
autoPushName: "提交后自动推送",
117142
autoPushDesc: "提交成功后自动推送到远程仓库。",
118143

144+
autoPullOnOpenName: "打开时自动拉取",
145+
autoPullOnOpenDesc: "打开 Obsidian 时自动从远程仓库拉取。",
146+
119147
templateName: "提交消息模板",
120148
templateDesc: "变量: {{date}}, {{time}}, {{files}}, {{count}}",
121149

@@ -140,15 +168,24 @@ const zhCN: Translations = {
140168
remoteUrlPlaceholder: "https://github.com/user/repo.git",
141169
saveButton: "保存",
142170

171+
conflictStatusName: "检测到合并冲突",
172+
conflictStatusDesc: "请手动解决冲突后,点击下方按钮。",
173+
resolveConflictButton: "标记为已解决",
174+
143175
noticeNoChanges: "GitAutoCommit: 没有可提交的更改。",
144176
noticeCommitted: (count) => `GitAutoCommit: 已提交 ${count} 个文件。`,
145177
noticePushed: "GitAutoCommit: 已推送到远程仓库。",
178+
noticePulled: "GitAutoCommit: 已从远程仓库拉取。",
146179
noticeAutoGitError: (msg) => `GitAutoCommit: 错误 - ${msg}`,
147180
noticePushFailed: (msg) => `GitAutoCommit: 推送失败 - ${msg}`,
181+
noticePullFailed: (msg) => `GitAutoCommit: 拉取失败 - ${msg}`,
148182
noticeMobileNotSupported: "GitAutoCommit: 移动端不支持 Git。",
149183
noticeDesktopOnly: "GitAutoCommit: 需要桌面端。",
150184
noticeRepoInitialized: "GitAutoCommit: 仓库已初始化。",
151185
noticeRemoteSaved: "GitAutoCommit: 远程地址已保存。",
186+
noticeConflictDetected: "GitAutoCommit: 检测到合并冲突!请手动解决。",
187+
noticeConflictResolved: "GitAutoCommit: 冲突已标记为解决。",
188+
noticeCannotCommitConflict: "GitAutoCommit: 存在冲突时无法提交。",
152189
};
153190

154191
const translations: Record<string, Translations> = {

src/main.ts

Lines changed: 109 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { EventRef, Notice, Platform, Plugin, TAbstractFile, TFile, FileSystemAdapter } from "obsidian";
22
import { AutoGitSettings, AutoGitSettingTab, DEFAULT_SETTINGS } from "./settings";
3-
import { getChangedFiles, commitAll, push, getFileStatuses, FileStatus } from "./git";
3+
import { getChangedFiles, commitAll, push, pull, getFileStatuses, getConflictFiles, hasConflicts, markConflictsResolved, FileStatus } from "./git";
44
import { renderTemplate } from "./template";
55
import { t } from "./i18n";
66

@@ -13,6 +13,9 @@ export default class AutoGitPlugin extends Plugin {
1313
private vaultEventRefs: EventRef[] = [];
1414
private statusRefreshInterval: number | null = null;
1515
private currentStatuses: Map<string, FileStatus> = new Map();
16+
private conflictFiles: Set<string> = new Set();
17+
private _hasConflicts = false;
18+
private resolveConflictCommand: { id: string } | null = null;
1619

1720
async onload() {
1821
await this.loadSettings();
@@ -35,8 +38,23 @@ export default class AutoGitPlugin extends Plugin {
3538
},
3639
});
3740

41+
this.addCommand({
42+
id: "pull-now",
43+
name: "Pull now",
44+
callback: () => this.doPull(),
45+
});
46+
3847
this.setupVaultListeners();
3948
this.setupStatusBadges();
49+
50+
// Auto pull on open
51+
if (this.settings.autoPullOnOpen && !Platform.isMobileApp) {
52+
// Delay to ensure vault is ready
53+
window.setTimeout(() => this.doPull(), 1000);
54+
}
55+
56+
// Check for existing conflicts on load
57+
this.checkConflicts();
4058
}
4159

4260
onunload() {
@@ -131,6 +149,14 @@ export default class AutoGitPlugin extends Plugin {
131149
}
132150

133151
async runCommit(reason: "manual" | "auto"): Promise<boolean> {
152+
// Don't commit if there are conflicts
153+
if (this._hasConflicts) {
154+
if (reason === "manual") {
155+
new Notice(t().noticeCannotCommitConflict);
156+
}
157+
return false;
158+
}
159+
134160
if (this.isCommitting) {
135161
this.pendingRerun = true;
136162
return false;
@@ -197,6 +223,71 @@ export default class AutoGitPlugin extends Plugin {
197223
}
198224
}
199225

226+
private async doPull() {
227+
if (Platform.isMobileApp) {
228+
new Notice(t().noticeMobileNotSupported);
229+
return;
230+
}
231+
232+
try {
233+
const cwd = this.getVaultPath();
234+
const result = await pull(cwd, this.settings.gitPath);
235+
236+
if (result.hasConflicts) {
237+
await this.checkConflicts();
238+
new Notice(t().noticeConflictDetected);
239+
} else if (result.success) {
240+
new Notice(t().noticePulled);
241+
this.refreshStatusBadges();
242+
}
243+
} catch (e) {
244+
new Notice(t().noticePullFailed((e as Error).message));
245+
}
246+
}
247+
248+
private async checkConflicts() {
249+
const cwd = this.getVaultPathSafe();
250+
if (!cwd) return;
251+
252+
const conflicts = await getConflictFiles(cwd, this.settings.gitPath);
253+
this.conflictFiles = new Set(conflicts);
254+
this.setHasConflicts(conflicts.length > 0);
255+
this.refreshStatusBadges();
256+
}
257+
258+
setHasConflicts(value: boolean) {
259+
this._hasConflicts = value;
260+
261+
if (value && !this.resolveConflictCommand) {
262+
// Add resolve conflict command when conflicts exist
263+
this.resolveConflictCommand = this.addCommand({
264+
id: "resolve-conflicts",
265+
name: "Mark conflicts as resolved",
266+
callback: async () => {
267+
const cwd = this.getVaultPathSafe();
268+
if (!cwd) return;
269+
try {
270+
await markConflictsResolved(cwd, this.settings.gitPath);
271+
this.conflictFiles.clear();
272+
this.setHasConflicts(false);
273+
new Notice(t().noticeConflictResolved);
274+
this.refreshStatusBadges();
275+
} catch (e) {
276+
new Notice((e as Error).message);
277+
}
278+
},
279+
});
280+
} else if (!value && this.resolveConflictCommand) {
281+
// Remove command when no conflicts - Note: Obsidian doesn't support removing commands
282+
// So we just clear the reference
283+
this.resolveConflictCommand = null;
284+
}
285+
286+
if (!value) {
287+
this.conflictFiles.clear();
288+
}
289+
}
290+
200291
// Status badge functionality
201292
private setupStatusBadges() {
202293
if (Platform.isMobileApp || !this.settings.showStatusBadge) return;
@@ -240,15 +331,21 @@ export default class AutoGitPlugin extends Plugin {
240331
// Remove old badges
241332
document.querySelectorAll(".git-status-badge").forEach((el) => el.remove());
242333

243-
// Calculate folder statuses from file statuses
244-
const folderStatuses = this.calculateFolderStatuses();
334+
// Merge conflict files into statuses with highest priority
335+
const mergedStatuses = new Map(this.currentStatuses);
336+
this.conflictFiles.forEach((file) => {
337+
mergedStatuses.set(file, "U" as FileStatus); // U for unmerged/conflict
338+
});
339+
340+
// Calculate folder statuses from merged statuses
341+
const folderStatuses = this.calculateFolderStatuses(mergedStatuses);
245342

246343
// Add badges to files
247344
document.querySelectorAll(".nav-file-title").forEach((item) => {
248345
const pathAttr = item.getAttribute("data-path");
249346
if (!pathAttr) return;
250347

251-
const status = this.currentStatuses.get(pathAttr);
348+
const status = mergedStatuses.get(pathAttr);
252349
if (status) {
253350
this.addBadgeToElement(item, status);
254351
}
@@ -266,16 +363,17 @@ export default class AutoGitPlugin extends Plugin {
266363
});
267364
}
268365

269-
private calculateFolderStatuses(): Map<string, FileStatus> {
366+
private calculateFolderStatuses(statuses?: Map<string, FileStatus>): Map<string, FileStatus> {
367+
const sourceStatuses = statuses || this.currentStatuses;
270368
const folderStatuses = new Map<string, FileStatus>();
271369

272-
this.currentStatuses.forEach((status, filePath) => {
370+
sourceStatuses.forEach((status, filePath) => {
273371
// Get all parent folders
274372
const parts = filePath.split(/[/\\]/);
275373
for (let i = 1; i < parts.length; i++) {
276374
const folderPath = parts.slice(0, i).join("/");
277375
const existing = folderStatuses.get(folderPath);
278-
// Priority: A > M > R > D
376+
// Priority: U (conflict) > A > M > R > D
279377
if (!existing || this.statusPriority(status) > this.statusPriority(existing)) {
280378
folderStatuses.set(folderPath, status);
281379
}
@@ -287,6 +385,7 @@ export default class AutoGitPlugin extends Plugin {
287385

288386
private statusPriority(status: FileStatus): number {
289387
switch (status) {
388+
case "U": return 5; // Conflict - highest priority
290389
case "A": return 4;
291390
case "M": return 3;
292391
case "R": return 2;
@@ -300,7 +399,9 @@ export default class AutoGitPlugin extends Plugin {
300399
badge.className = "git-status-badge";
301400
badge.textContent = "●";
302401

303-
if (status === "M") {
402+
if (status === "U") {
403+
badge.classList.add("conflict");
404+
} else if (status === "M") {
304405
badge.classList.add("modified");
305406
} else if (status === "A") {
306407
badge.classList.add("added");

0 commit comments

Comments
 (0)