11import { EventRef , Notice , Platform , Plugin , TAbstractFile , TFile , FileSystemAdapter } from "obsidian" ;
22import { 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" ;
44import { renderTemplate } from "./template" ;
55import { 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