fortrangoingonforty/fuss / 4742ff7

Browse files

diff, fetch, pull, fzf dialog to prompt for upstream info so it doesn't break all the time

Authored by espadonne
SHA
4742ff7689c41ca3550c30b4bdd6185340597eb7
Parents
1b06a83
Tree
cc09395

5 changed files

StatusFile+-
M src/display_module.f90 22 6
M src/fuss_main.f90 39 1
M src/git_module.f90 214 0
M src/tree_module.f90 7 4
M src/types_module.f90 3 0
src/display_module.f90modified
@@ -20,12 +20,13 @@ contains
20
         root%is_staged = .false.
20
         root%is_staged = .false.
21
         root%is_unstaged = .false.
21
         root%is_unstaged = .false.
22
         root%is_untracked = .false.
22
         root%is_untracked = .false.
23
+        root%has_incoming = .false.
23
         root%first_child => null()
24
         root%first_child => null()
24
         root%next_sibling => null()
25
         root%next_sibling => null()
25
 
26
 
26
         ! Build tree
27
         ! Build tree
27
         do i = 1, n_files
28
         do i = 1, n_files
28
-            call add_to_tree(root, files(i)%path, files(i)%is_staged, files(i)%is_unstaged, files(i)%is_untracked)
29
+            call add_to_tree(root, files(i)%path, files(i)%is_staged, files(i)%is_unstaged, files(i)%is_untracked, files(i)%has_incoming)
29
         end do
30
         end do
30
 
31
 
31
         ! Sort tree
32
         ! Sort tree
@@ -58,10 +59,12 @@ contains
58
         character(len=50) :: mark_unstaged
59
         character(len=50) :: mark_unstaged
59
         character(len=50) :: mark_untracked
60
         character(len=50) :: mark_untracked
60
         character(len=50) :: mark_staged
61
         character(len=50) :: mark_staged
62
+        character(len=50) :: mark_incoming
61
 
63
 
62
         write(mark_unstaged, '(A,A,A,A,A)') ESC, '[31m', ' ✗', ESC, '[0m'
64
         write(mark_unstaged, '(A,A,A,A,A)') ESC, '[31m', ' ✗', ESC, '[0m'
63
         write(mark_untracked, '(A,A,A,A,A)') ESC, '[90m', ' ✗', ESC, '[0m'
65
         write(mark_untracked, '(A,A,A,A,A)') ESC, '[90m', ' ✗', ESC, '[0m'
64
         write(mark_staged, '(A,A,A,A,A)') ESC, '[32m', ' ↑', ESC, '[0m'
66
         write(mark_staged, '(A,A,A,A,A)') ESC, '[32m', ' ↑', ESC, '[0m'
67
+        write(mark_incoming, '(A,A,A,A,A)') ESC, '[34m', ' ↓', ESC, '[0m'
65
 
68
 
66
         ! Count children first
69
         ! Count children first
67
         n_children = 0
70
         n_children = 0
@@ -90,6 +93,9 @@ contains
90
             if (node%is_untracked) then
93
             if (node%is_untracked) then
91
                 line = trim(line) // trim(mark_untracked)
94
                 line = trim(line) // trim(mark_untracked)
92
             end if
95
             end if
96
+            if (node%has_incoming) then
97
+                line = trim(line) // trim(mark_incoming)
98
+            end if
93
             print '(A)', trim(line)
99
             print '(A)', trim(line)
94
         end if
100
         end if
95
 
101
 
@@ -132,11 +138,12 @@ contains
132
         root%is_staged = .false.
138
         root%is_staged = .false.
133
         root%is_unstaged = .false.
139
         root%is_unstaged = .false.
134
         root%is_untracked = .false.
140
         root%is_untracked = .false.
141
+        root%has_incoming = .false.
135
         root%first_child => null()
142
         root%first_child => null()
136
         root%next_sibling => null()
143
         root%next_sibling => null()
137
 
144
 
138
         do i = 1, n_files
145
         do i = 1, n_files
139
-            call add_to_tree(root, files(i)%path, files(i)%is_staged, files(i)%is_unstaged, files(i)%is_untracked)
146
+            call add_to_tree(root, files(i)%path, files(i)%is_staged, files(i)%is_unstaged, files(i)%is_untracked, files(i)%has_incoming)
140
         end do
147
         end do
141
 
148
 
142
         call sort_tree(root)
149
         call sort_tree(root)
@@ -161,12 +168,13 @@ contains
161
         call print_interactive_node(root, '', .true., .true., items, selected, &
168
         call print_interactive_node(root, '', .true., .true., items, selected, &
162
                                     item_idx, viewport_offset, viewport_end)
169
                                     item_idx, viewport_offset, viewport_end)
163
 
170
 
164
-        ! Print help
171
+        ! Print help (two rows for better readability)
165
         print '(A)', ''
172
         print '(A)', ''
166
-        print '(A)', achar(27) // '[32m↑' // achar(27) // '[0m=staged ' // &
173
+        print '(A)', 'Legend: ' // achar(27) // '[32m↑' // achar(27) // '[0m=staged ' // &
167
                      achar(27) // '[31m✗' // achar(27) // '[0m=modified ' // &
174
                      achar(27) // '[31m✗' // achar(27) // '[0m=modified ' // &
168
-                     achar(27) // '[90m✗' // achar(27) // '[0m=untracked'
175
+                     achar(27) // '[90m✗' // achar(27) // '[0m=untracked ' // &
169
-        print '(A)', 'j/k/↓/↑: navigate | a: stage | u: unstage | m: commit | p: push | s: status | q: quit'
176
+                     achar(27) // '[34m↓' // achar(27) // '[0m=incoming'
177
+        print '(A)', 'Keys: j/k/↑/↓:nav | a:stage | u:unstage | f:fetch | d:diff | l:pull | m:commit | p:push | s:status | q:quit'
170
 
178
 
171
         call free_tree(root)
179
         call free_tree(root)
172
     end subroutine draw_interactive_tree
180
     end subroutine draw_interactive_tree
@@ -197,10 +205,12 @@ contains
197
         character(len=50) :: mark_unstaged
205
         character(len=50) :: mark_unstaged
198
         character(len=50) :: mark_untracked
206
         character(len=50) :: mark_untracked
199
         character(len=50) :: mark_staged
207
         character(len=50) :: mark_staged
208
+        character(len=50) :: mark_incoming
200
 
209
 
201
         write(mark_unstaged, '(A,A,A,A,A)') ESC, '[31m', ' ✗', ESC, '[0m'
210
         write(mark_unstaged, '(A,A,A,A,A)') ESC, '[31m', ' ✗', ESC, '[0m'
202
         write(mark_untracked, '(A,A,A,A,A)') ESC, '[90m', ' ✗', ESC, '[0m'
211
         write(mark_untracked, '(A,A,A,A,A)') ESC, '[90m', ' ✗', ESC, '[0m'
203
         write(mark_staged, '(A,A,A,A,A)') ESC, '[32m', ' ↑', ESC, '[0m'
212
         write(mark_staged, '(A,A,A,A,A)') ESC, '[32m', ' ↑', ESC, '[0m'
213
+        write(mark_incoming, '(A,A,A,A,A)') ESC, '[34m', ' ↓', ESC, '[0m'
204
 
214
 
205
         ! Count children first
215
         ! Count children first
206
         n_children = 0
216
         n_children = 0
@@ -237,6 +247,9 @@ contains
237
                     if (node%is_untracked) then
247
                     if (node%is_untracked) then
238
                         line = trim(line) // trim(mark_untracked)
248
                         line = trim(line) // trim(mark_untracked)
239
                     end if
249
                     end if
250
+                    if (node%has_incoming) then
251
+                        line = trim(line) // trim(mark_incoming)
252
+                    end if
240
                     line = trim(line) // highlight_off
253
                     line = trim(line) // highlight_off
241
                 else
254
                 else
242
                     line = trim(line) // trim(node%name)
255
                     line = trim(line) // trim(node%name)
@@ -249,6 +262,9 @@ contains
249
                     if (node%is_untracked) then
262
                     if (node%is_untracked) then
250
                         line = trim(line) // trim(mark_untracked)
263
                         line = trim(line) // trim(mark_untracked)
251
                     end if
264
                     end if
265
+                    if (node%has_incoming) then
266
+                        line = trim(line) // trim(mark_incoming)
267
+                    end if
252
                 end if
268
                 end if
253
 
269
 
254
                 print '(A)', trim(line)
270
                 print '(A)', trim(line)
src/fuss_main.f90modified
@@ -72,6 +72,9 @@ contains
72
             call get_dirty_files(files, n_files)
72
             call get_dirty_files(files, n_files)
73
         end if
73
         end if
74
 
74
 
75
+        ! Mark files with incoming changes
76
+        call mark_incoming_changes(files, n_files)
77
+
75
         ! Display the tree
78
         ! Display the tree
76
         if (n_files > 0) then
79
         if (n_files > 0) then
77
             print '(A)', '.'
80
             print '(A)', '.'
@@ -107,6 +110,9 @@ contains
107
             call get_dirty_files(files, n_files)
110
             call get_dirty_files(files, n_files)
108
         end if
111
         end if
109
 
112
 
113
+        ! Mark files with incoming changes
114
+        call mark_incoming_changes(files, n_files)
115
+
110
         if (n_files == 0) then
116
         if (n_files == 0) then
111
             print '(A)', 'No files to display'
117
             print '(A)', 'No files to display'
112
             return
118
             return
@@ -171,6 +177,7 @@ contains
171
                     else
177
                     else
172
                         call get_dirty_files(files, n_files)
178
                         call get_dirty_files(files, n_files)
173
                     end if
179
                     end if
180
+                    call mark_incoming_changes(files, n_files)
174
                     call build_item_list(files, n_files, items, n_items)
181
                     call build_item_list(files, n_files, items, n_items)
175
                     if (selected > n_items .and. n_items > 0) selected = n_items
182
                     if (selected > n_items .and. n_items > 0) selected = n_items
176
                     if (n_items == 0) running = .false.
183
                     if (n_items == 0) running = .false.
@@ -184,6 +191,7 @@ contains
184
                     else
191
                     else
185
                         call get_dirty_files(files, n_files)
192
                         call get_dirty_files(files, n_files)
186
                     end if
193
                     end if
194
+                    call mark_incoming_changes(files, n_files)
187
                     call build_item_list(files, n_files, items, n_items)
195
                     call build_item_list(files, n_files, items, n_items)
188
                     if (selected > n_items .and. n_items > 0) selected = n_items
196
                     if (selected > n_items .and. n_items > 0) selected = n_items
189
                 end if
197
                 end if
@@ -195,6 +203,7 @@ contains
195
                 else
203
                 else
196
                     call get_dirty_files(files, n_files)
204
                     call get_dirty_files(files, n_files)
197
                 end if
205
                 end if
206
+                call mark_incoming_changes(files, n_files)
198
                 call build_item_list(files, n_files, items, n_items)
207
                 call build_item_list(files, n_files, items, n_items)
199
                 if (selected > n_items .and. n_items > 0) selected = n_items
208
                 if (selected > n_items .and. n_items > 0) selected = n_items
200
             case ('s')  ! Show git status (lowercase)
209
             case ('s')  ! Show git status (lowercase)
@@ -207,6 +216,33 @@ contains
207
                 else
216
                 else
208
                     call get_dirty_files(files, n_files)
217
                     call get_dirty_files(files, n_files)
209
                 end if
218
                 end if
219
+                call mark_incoming_changes(files, n_files)
220
+                call build_item_list(files, n_files, items, n_items)
221
+                if (selected > n_items .and. n_items > 0) selected = n_items
222
+            case ('f')  ! Git fetch
223
+                call git_fetch()
224
+                ! Refresh files after fetch
225
+                if (show_all) then
226
+                    call get_all_files(files, n_files)
227
+                else
228
+                    call get_dirty_files(files, n_files)
229
+                end if
230
+                call mark_incoming_changes(files, n_files)
231
+                call build_item_list(files, n_files, items, n_items)
232
+                if (selected > n_items .and. n_items > 0) selected = n_items
233
+            case ('d')  ! Git diff with less
234
+                if (items(selected)%is_file) then
235
+                    call git_diff_file(items(selected)%path, items(selected)%has_incoming)
236
+                end if
237
+            case ('l')  ! Git pull
238
+                call git_pull()
239
+                ! Refresh files after pull
240
+                if (show_all) then
241
+                    call get_all_files(files, n_files)
242
+                else
243
+                    call get_dirty_files(files, n_files)
244
+                end if
245
+                call mark_incoming_changes(files, n_files)
210
                 call build_item_list(files, n_files, items, n_items)
246
                 call build_item_list(files, n_files, items, n_items)
211
                 if (selected > n_items .and. n_items > 0) selected = n_items
247
                 if (selected > n_items .and. n_items > 0) selected = n_items
212
             case ('q', 'Q')  ! Quit
248
             case ('q', 'Q')  ! Quit
@@ -238,11 +274,12 @@ contains
238
         root%is_staged = .false.
274
         root%is_staged = .false.
239
         root%is_unstaged = .false.
275
         root%is_unstaged = .false.
240
         root%is_untracked = .false.
276
         root%is_untracked = .false.
277
+        root%has_incoming = .false.
241
         root%first_child => null()
278
         root%first_child => null()
242
         root%next_sibling => null()
279
         root%next_sibling => null()
243
 
280
 
244
         do i = 1, n_files
281
         do i = 1, n_files
245
-            call add_to_tree(root, files(i)%path, files(i)%is_staged, files(i)%is_unstaged, files(i)%is_untracked)
282
+            call add_to_tree(root, files(i)%path, files(i)%is_staged, files(i)%is_unstaged, files(i)%is_untracked, files(i)%has_incoming)
246
         end do
283
         end do
247
 
284
 
248
         call sort_tree(root)
285
         call sort_tree(root)
@@ -291,6 +328,7 @@ contains
291
             items(n_items)%is_staged = node%is_staged
328
             items(n_items)%is_staged = node%is_staged
292
             items(n_items)%is_unstaged = node%is_unstaged
329
             items(n_items)%is_unstaged = node%is_unstaged
293
             items(n_items)%is_untracked = node%is_untracked
330
             items(n_items)%is_untracked = node%is_untracked
331
+            items(n_items)%has_incoming = node%has_incoming
294
         else
332
         else
295
             full_path = ''
333
             full_path = ''
296
         end if
334
         end if
src/git_module.f90modified
@@ -69,6 +69,7 @@ contains
69
                 temp_files(n_files)%is_untracked = (git_status == '??')
69
                 temp_files(n_files)%is_untracked = (git_status == '??')
70
                 temp_files(n_files)%is_staged = (git_status(1:1) /= ' ' .and. git_status(1:1) /= '?')
70
                 temp_files(n_files)%is_staged = (git_status(1:1) /= ' ' .and. git_status(1:1) /= '?')
71
                 temp_files(n_files)%is_unstaged = (git_status(2:2) /= ' ' .and. .not. temp_files(n_files)%is_untracked)
71
                 temp_files(n_files)%is_unstaged = (git_status(2:2) /= ' ' .and. .not. temp_files(n_files)%is_untracked)
72
+                temp_files(n_files)%has_incoming = .false.
72
             end if
73
             end if
73
         end do
74
         end do
74
 
75
 
@@ -142,6 +143,7 @@ contains
142
                 temp_files(n_files)%is_staged = .false.
143
                 temp_files(n_files)%is_staged = .false.
143
                 temp_files(n_files)%is_unstaged = .false.
144
                 temp_files(n_files)%is_unstaged = .false.
144
                 temp_files(n_files)%is_untracked = .false.
145
                 temp_files(n_files)%is_untracked = .false.
146
+                temp_files(n_files)%has_incoming = .false.
145
                 do i = 1, n_dirty
147
                 do i = 1, n_dirty
146
                     if (trim(dirty_files(i)%path) == trim(line)) then
148
                     if (trim(dirty_files(i)%path) == trim(line)) then
147
                         is_dirty_file = .true.
149
                         is_dirty_file = .true.
@@ -149,6 +151,7 @@ contains
149
                         temp_files(n_files)%is_staged = dirty_files(i)%is_staged
151
                         temp_files(n_files)%is_staged = dirty_files(i)%is_staged
150
                         temp_files(n_files)%is_unstaged = dirty_files(i)%is_unstaged
152
                         temp_files(n_files)%is_unstaged = dirty_files(i)%is_unstaged
151
                         temp_files(n_files)%is_untracked = dirty_files(i)%is_untracked
153
                         temp_files(n_files)%is_untracked = dirty_files(i)%is_untracked
154
+                        temp_files(n_files)%has_incoming = dirty_files(i)%has_incoming
152
                         exit
155
                         exit
153
                     end if
156
                     end if
154
                 end do
157
                 end do
@@ -223,6 +226,7 @@ contains
223
                 files(n_files)%is_untracked = (git_status == '??')
226
                 files(n_files)%is_untracked = (git_status == '??')
224
                 files(n_files)%is_staged = (git_status(1:1) /= ' ' .and. git_status(1:1) /= '?')
227
                 files(n_files)%is_staged = (git_status(1:1) /= ' ' .and. git_status(1:1) /= '?')
225
                 files(n_files)%is_unstaged = (git_status(2:2) /= ' ' .and. .not. files(n_files)%is_untracked)
228
                 files(n_files)%is_unstaged = (git_status(2:2) /= ' ' .and. .not. files(n_files)%is_untracked)
229
+                files(n_files)%has_incoming = .false.
226
             end if
230
             end if
227
         end do
231
         end do
228
 
232
 
@@ -402,4 +406,214 @@ contains
402
         end if
406
         end if
403
     end subroutine get_repo_info
407
     end subroutine get_repo_info
404
 
408
 
409
+    subroutine prompt_upstream_selection(success)
410
+        logical, intent(out) :: success
411
+        integer :: status_code
412
+        character(len=512) :: selected_branch
413
+
414
+        success = .false.
415
+
416
+        print '(A)', ''
417
+        print '(A)', 'No upstream branch configured for this branch.'
418
+        print '(A)', 'Select a remote branch to track:'
419
+        print '(A)', ''
420
+
421
+        ! Restore terminal for fzf
422
+        call execute_command_line('stty sane < /dev/tty', exitstat=status_code)
423
+
424
+        ! Use fzf to select remote branch
425
+        call execute_command_line('git branch -r | grep -v HEAD | sed "s/^  //" | ' // &
426
+                                  'fzf --height=10 --prompt="Select upstream: " > /tmp/fuss_upstream.txt', &
427
+                                  exitstat=status_code)
428
+
429
+        if (status_code /= 0) then
430
+            print '(A)', 'No upstream selected.'
431
+            call execute_command_line('sleep 1', exitstat=status_code)
432
+            ! Re-enable cbreak mode
433
+            call execute_command_line('stty cbreak -echo < /dev/tty', exitstat=status_code)
434
+            return
435
+        end if
436
+
437
+        ! Read selected branch
438
+        open(unit=99, file='/tmp/fuss_upstream.txt', status='old', action='read', iostat=status_code)
439
+        if (status_code == 0) then
440
+            read(99, '(A)', iostat=status_code) selected_branch
441
+            close(99, status='delete')
442
+
443
+            if (status_code == 0 .and. len_trim(selected_branch) > 0) then
444
+                ! Set upstream
445
+                call execute_command_line('git branch --set-upstream-to=' // trim(selected_branch), &
446
+                                          exitstat=status_code)
447
+
448
+                if (status_code == 0) then
449
+                    print '(A)', achar(27) // '[32m✓ Upstream set to: ' // trim(selected_branch) // achar(27) // '[0m'
450
+                    success = .true.
451
+                else
452
+                    print '(A)', achar(27) // '[31m✗ Failed to set upstream' // achar(27) // '[0m'
453
+                end if
454
+                call execute_command_line('sleep 1', exitstat=status_code)
455
+            end if
456
+        end if
457
+
458
+        ! Re-enable cbreak mode
459
+        call execute_command_line('stty cbreak -echo < /dev/tty', exitstat=status_code)
460
+    end subroutine prompt_upstream_selection
461
+
462
+    subroutine mark_incoming_changes(files, n_files)
463
+        type(file_entry), intent(inout) :: files(:)
464
+        integer, intent(in) :: n_files
465
+        integer :: iostat, unit_num, status_code, i
466
+        character(len=1024) :: line
467
+        character(len=512) :: incoming_path
468
+        logical :: upstream_set
469
+
470
+        ! Check if there's an upstream branch configured
471
+        call execute_command_line('git rev-parse --abbrev-ref @{upstream} > /dev/null 2>&1', exitstat=status_code)
472
+        if (status_code /= 0) then
473
+            ! No upstream configured - prompt user to select one
474
+            call prompt_upstream_selection(upstream_set)
475
+            if (.not. upstream_set) return
476
+        end if
477
+
478
+        ! Get list of files that differ between HEAD and upstream
479
+        call execute_command_line('git diff --name-only HEAD...@{upstream} > /tmp/fuss_incoming.txt 2>/dev/null', &
480
+                                  exitstat=status_code)
481
+
482
+        if (status_code /= 0) then
483
+            ! If diff fails, no incoming changes
484
+            return
485
+        end if
486
+
487
+        open(newunit=unit_num, file='/tmp/fuss_incoming.txt', status='old', action='read', iostat=iostat)
488
+        if (iostat /= 0) return
489
+
490
+        do
491
+            read(unit_num, '(A)', iostat=iostat) line
492
+            if (iostat /= 0) exit
493
+
494
+            if (len_trim(line) > 0) then
495
+                incoming_path = trim(line)
496
+                ! Mark this file as having incoming changes
497
+                do i = 1, n_files
498
+                    if (trim(files(i)%path) == trim(incoming_path)) then
499
+                        files(i)%has_incoming = .true.
500
+                        exit
501
+                    end if
502
+                end do
503
+            end if
504
+        end do
505
+
506
+        close(unit_num, status='delete')
507
+    end subroutine mark_incoming_changes
508
+
509
+    subroutine git_fetch()
510
+        integer :: status
511
+        logical :: upstream_set
512
+
513
+        ! Check if there's an upstream branch configured
514
+        call execute_command_line('git rev-parse --abbrev-ref @{upstream} > /dev/null 2>&1', exitstat=status)
515
+        if (status /= 0) then
516
+            ! No upstream configured - prompt user to select one
517
+            call prompt_upstream_selection(upstream_set)
518
+            if (.not. upstream_set) return
519
+        end if
520
+
521
+        ! Run git fetch
522
+        print '(A)', 'Fetching from remote...'
523
+        call execute_command_line('git fetch', exitstat=status)
524
+
525
+        if (status == 0) then
526
+            print '(A)', achar(27) // '[32m✓ Fetch completed!' // achar(27) // '[0m'
527
+        else
528
+            print '(A)', achar(27) // '[31m✗ Fetch failed!' // achar(27) // '[0m'
529
+        end if
530
+
531
+        ! Brief pause to show message
532
+        call execute_command_line('sleep 1', exitstat=status)
533
+    end subroutine git_fetch
534
+
535
+    subroutine git_pull()
536
+        integer :: status
537
+        logical :: upstream_set
538
+
539
+        ! Check if there's an upstream branch configured
540
+        call execute_command_line('git rev-parse --abbrev-ref @{upstream} > /dev/null 2>&1', exitstat=status)
541
+        if (status /= 0) then
542
+            ! No upstream configured - prompt user to select one
543
+            call prompt_upstream_selection(upstream_set)
544
+            if (.not. upstream_set) return
545
+        end if
546
+
547
+        ! Run git pull
548
+        print '(A)', 'Pulling from remote...'
549
+        call execute_command_line('git pull', exitstat=status)
550
+
551
+        if (status == 0) then
552
+            print '(A)', achar(27) // '[32m✓ Pull completed!' // achar(27) // '[0m'
553
+        else
554
+            print '(A)', achar(27) // '[31m✗ Pull failed!' // achar(27) // '[0m'
555
+        end if
556
+
557
+        ! Brief pause to show message
558
+        call execute_command_line('sleep 1', exitstat=status)
559
+    end subroutine git_pull
560
+
561
+    subroutine git_diff_file(filepath, has_incoming)
562
+        character(len=*), intent(in) :: filepath
563
+        logical, intent(in) :: has_incoming
564
+        character(len=2048) :: command
565
+        integer :: status
566
+        logical :: upstream_set
567
+        logical :: has_local_changes
568
+
569
+        ! Restore terminal temporarily for less
570
+        call execute_command_line('stty sane < /dev/tty', exitstat=status)
571
+
572
+        ! Check if file has local changes (unstaged or staged)
573
+        call execute_command_line('git status --porcelain -- "' // trim(filepath) // '" | grep -q "^.M\|^M"', &
574
+                                  exitstat=status)
575
+        has_local_changes = (status == 0)
576
+
577
+        if (has_local_changes .and. has_incoming) then
578
+            ! Show both local changes and incoming changes
579
+            print '(A)', achar(27) // '[1;33mShowing LOCAL changes (working tree vs HEAD):' // achar(27) // '[0m'
580
+            print '(A)', ''
581
+            write(command, '(A,A,A)') '(git diff HEAD -- "', trim(filepath), '" && echo "" && echo "' // &
582
+                                      achar(27) // '[1;33m=== INCOMING changes (upstream vs HEAD) ===' // achar(27) // &
583
+                                      '[0m" && echo "" && git diff HEAD...@{upstream} -- "', trim(filepath), &
584
+                                      '") | less -R'
585
+            call execute_command_line(trim(command), exitstat=status)
586
+        else if (has_local_changes) then
587
+            ! Show local changes only (working tree vs HEAD)
588
+            print '(A)', achar(27) // '[1;33mShowing LOCAL changes (working tree vs HEAD):' // achar(27) // '[0m'
589
+            print '(A)', ''
590
+            write(command, '(A,A,A)') 'git diff HEAD -- "', trim(filepath), '" | less -R'
591
+            call execute_command_line(trim(command), exitstat=status)
592
+        else if (has_incoming) then
593
+            ! Show incoming changes only (upstream vs HEAD)
594
+            ! Check if there's an upstream branch configured
595
+            call execute_command_line('git rev-parse --abbrev-ref @{upstream} > /dev/null 2>&1', exitstat=status)
596
+            if (status /= 0) then
597
+                ! No upstream configured - prompt user to select one
598
+                call prompt_upstream_selection(upstream_set)
599
+                if (.not. upstream_set) then
600
+                    call execute_command_line('stty cbreak -echo < /dev/tty', exitstat=status)
601
+                    return
602
+                end if
603
+            end if
604
+
605
+            print '(A)', achar(27) // '[1;33mShowing INCOMING changes (upstream vs HEAD):' // achar(27) // '[0m'
606
+            print '(A)', ''
607
+            write(command, '(A,A,A)') 'git diff HEAD...@{upstream} -- "', trim(filepath), '" | less -R'
608
+            call execute_command_line(trim(command), exitstat=status)
609
+        else
610
+            ! No changes to show
611
+            print '(A)', achar(27) // '[33mNo changes to show for: ' // trim(filepath) // achar(27) // '[0m'
612
+            call execute_command_line('sleep 1', exitstat=status)
613
+        end if
614
+
615
+        ! Re-enable cbreak mode
616
+        call execute_command_line('stty cbreak -echo < /dev/tty', exitstat=status)
617
+    end subroutine git_diff_file
618
+
405
 end module git_module
619
 end module git_module
src/tree_module.f90modified
@@ -4,10 +4,10 @@ module tree_module
4
 
4
 
5
 contains
5
 contains
6
 
6
 
7
-    recursive subroutine add_to_tree(node, path, is_staged, is_unstaged, is_untracked)
7
+    recursive subroutine add_to_tree(node, path, is_staged, is_unstaged, is_untracked, has_incoming)
8
         type(tree_node), pointer, intent(in) :: node
8
         type(tree_node), pointer, intent(in) :: node
9
         character(len=*), intent(in) :: path
9
         character(len=*), intent(in) :: path
10
-        logical, intent(in) :: is_staged, is_unstaged, is_untracked
10
+        logical, intent(in) :: is_staged, is_unstaged, is_untracked, has_incoming
11
 
11
 
12
         integer :: slash_pos, iostat
12
         integer :: slash_pos, iostat
13
         character(len=512) :: first_part, rest
13
         character(len=512) :: first_part, rest
@@ -27,6 +27,7 @@ contains
27
                     child%is_staged = child%is_staged .or. is_staged
27
                     child%is_staged = child%is_staged .or. is_staged
28
                     child%is_unstaged = child%is_unstaged .or. is_unstaged
28
                     child%is_unstaged = child%is_unstaged .or. is_unstaged
29
                     child%is_untracked = child%is_untracked .or. is_untracked
29
                     child%is_untracked = child%is_untracked .or. is_untracked
30
+                    child%has_incoming = child%has_incoming .or. has_incoming
30
                     return
31
                     return
31
                 end if
32
                 end if
32
                 if (.not. associated(child%next_sibling)) exit
33
                 if (.not. associated(child%next_sibling)) exit
@@ -51,6 +52,7 @@ contains
51
             new_child%is_staged = is_staged
52
             new_child%is_staged = is_staged
52
             new_child%is_unstaged = is_unstaged
53
             new_child%is_unstaged = is_unstaged
53
             new_child%is_untracked = is_untracked
54
             new_child%is_untracked = is_untracked
55
+            new_child%has_incoming = has_incoming
54
             new_child%first_child => null()
56
             new_child%first_child => null()
55
             new_child%next_sibling => null()
57
             new_child%next_sibling => null()
56
 
58
 
@@ -68,7 +70,7 @@ contains
68
             child => node%first_child
70
             child => node%first_child
69
             do while (associated(child))
71
             do while (associated(child))
70
                 if (trim(child%name) == trim(first_part)) then
72
                 if (trim(child%name) == trim(first_part)) then
71
-                    call add_to_tree(child, rest, is_staged, is_unstaged, is_untracked)
73
+                    call add_to_tree(child, rest, is_staged, is_unstaged, is_untracked, has_incoming)
72
                     return
74
                     return
73
                 end if
75
                 end if
74
                 if (.not. associated(child%next_sibling)) exit
76
                 if (.not. associated(child%next_sibling)) exit
@@ -82,6 +84,7 @@ contains
82
             new_child%is_staged = .false.
84
             new_child%is_staged = .false.
83
             new_child%is_unstaged = .false.
85
             new_child%is_unstaged = .false.
84
             new_child%is_untracked = .false.
86
             new_child%is_untracked = .false.
87
+            new_child%has_incoming = .false.
85
             new_child%first_child => null()
88
             new_child%first_child => null()
86
             new_child%next_sibling => null()
89
             new_child%next_sibling => null()
87
 
90
 
@@ -91,7 +94,7 @@ contains
91
                 child%next_sibling => new_child
94
                 child%next_sibling => new_child
92
             end if
95
             end if
93
 
96
 
94
-            call add_to_tree(new_child, rest, is_staged, is_unstaged, is_untracked)
97
+            call add_to_tree(new_child, rest, is_staged, is_unstaged, is_untracked, has_incoming)
95
         end if
98
         end if
96
     end subroutine add_to_tree
99
     end subroutine add_to_tree
97
 
100
 
src/types_module.f90modified
@@ -8,6 +8,7 @@ module types_module
8
         logical :: is_staged
8
         logical :: is_staged
9
         logical :: is_unstaged
9
         logical :: is_unstaged
10
         logical :: is_untracked
10
         logical :: is_untracked
11
+        logical :: has_incoming
11
         type(tree_node), pointer :: first_child => null()
12
         type(tree_node), pointer :: first_child => null()
12
         type(tree_node), pointer :: next_sibling => null()
13
         type(tree_node), pointer :: next_sibling => null()
13
     end type tree_node
14
     end type tree_node
@@ -18,6 +19,7 @@ module types_module
18
         logical :: is_staged
19
         logical :: is_staged
19
         logical :: is_unstaged
20
         logical :: is_unstaged
20
         logical :: is_untracked
21
         logical :: is_untracked
22
+        logical :: has_incoming
21
     end type file_entry
23
     end type file_entry
22
 
24
 
23
     type :: selectable_item
25
     type :: selectable_item
@@ -25,6 +27,7 @@ module types_module
25
         logical :: is_staged
27
         logical :: is_staged
26
         logical :: is_unstaged
28
         logical :: is_unstaged
27
         logical :: is_untracked
29
         logical :: is_untracked
30
+        logical :: has_incoming
28
         logical :: is_file
31
         logical :: is_file
29
     end type selectable_item
32
     end type selectable_item
30
 
33