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
2020
         root%is_staged = .false.
2121
         root%is_unstaged = .false.
2222
         root%is_untracked = .false.
23
+        root%has_incoming = .false.
2324
         root%first_child => null()
2425
         root%next_sibling => null()
2526
 
2627
         ! Build tree
2728
         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)
2930
         end do
3031
 
3132
         ! Sort tree
@@ -58,10 +59,12 @@ contains
5859
         character(len=50) :: mark_unstaged
5960
         character(len=50) :: mark_untracked
6061
         character(len=50) :: mark_staged
62
+        character(len=50) :: mark_incoming
6163
 
6264
         write(mark_unstaged, '(A,A,A,A,A)') ESC, '[31m', ' ✗', ESC, '[0m'
6365
         write(mark_untracked, '(A,A,A,A,A)') ESC, '[90m', ' ✗', ESC, '[0m'
6466
         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'
6568
 
6669
         ! Count children first
6770
         n_children = 0
@@ -90,6 +93,9 @@ contains
9093
             if (node%is_untracked) then
9194
                 line = trim(line) // trim(mark_untracked)
9295
             end if
96
+            if (node%has_incoming) then
97
+                line = trim(line) // trim(mark_incoming)
98
+            end if
9399
             print '(A)', trim(line)
94100
         end if
95101
 
@@ -132,11 +138,12 @@ contains
132138
         root%is_staged = .false.
133139
         root%is_unstaged = .false.
134140
         root%is_untracked = .false.
141
+        root%has_incoming = .false.
135142
         root%first_child => null()
136143
         root%next_sibling => null()
137144
 
138145
         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)
140147
         end do
141148
 
142149
         call sort_tree(root)
@@ -161,12 +168,13 @@ contains
161168
         call print_interactive_node(root, '', .true., .true., items, selected, &
162169
                                     item_idx, viewport_offset, viewport_end)
163170
 
164
-        ! Print help
171
+        ! Print help (two rows for better readability)
165172
         print '(A)', ''
166
-        print '(A)', achar(27) // '[32m↑' // achar(27) // '[0m=staged ' // &
173
+        print '(A)', 'Legend: ' // achar(27) // '[32m↑' // achar(27) // '[0m=staged ' // &
167174
                      achar(27) // '[31m✗' // achar(27) // '[0m=modified ' // &
168
-                     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'
175
+                     achar(27) // '[90m✗' // achar(27) // '[0m=untracked ' // &
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'
170178
 
171179
         call free_tree(root)
172180
     end subroutine draw_interactive_tree
@@ -197,10 +205,12 @@ contains
197205
         character(len=50) :: mark_unstaged
198206
         character(len=50) :: mark_untracked
199207
         character(len=50) :: mark_staged
208
+        character(len=50) :: mark_incoming
200209
 
201210
         write(mark_unstaged, '(A,A,A,A,A)') ESC, '[31m', ' ✗', ESC, '[0m'
202211
         write(mark_untracked, '(A,A,A,A,A)') ESC, '[90m', ' ✗', ESC, '[0m'
203212
         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'
204214
 
205215
         ! Count children first
206216
         n_children = 0
@@ -237,6 +247,9 @@ contains
237247
                     if (node%is_untracked) then
238248
                         line = trim(line) // trim(mark_untracked)
239249
                     end if
250
+                    if (node%has_incoming) then
251
+                        line = trim(line) // trim(mark_incoming)
252
+                    end if
240253
                     line = trim(line) // highlight_off
241254
                 else
242255
                     line = trim(line) // trim(node%name)
@@ -249,6 +262,9 @@ contains
249262
                     if (node%is_untracked) then
250263
                         line = trim(line) // trim(mark_untracked)
251264
                     end if
265
+                    if (node%has_incoming) then
266
+                        line = trim(line) // trim(mark_incoming)
267
+                    end if
252268
                 end if
253269
 
254270
                 print '(A)', trim(line)
src/fuss_main.f90modified
@@ -72,6 +72,9 @@ contains
7272
             call get_dirty_files(files, n_files)
7373
         end if
7474
 
75
+        ! Mark files with incoming changes
76
+        call mark_incoming_changes(files, n_files)
77
+
7578
         ! Display the tree
7679
         if (n_files > 0) then
7780
             print '(A)', '.'
@@ -107,6 +110,9 @@ contains
107110
             call get_dirty_files(files, n_files)
108111
         end if
109112
 
113
+        ! Mark files with incoming changes
114
+        call mark_incoming_changes(files, n_files)
115
+
110116
         if (n_files == 0) then
111117
             print '(A)', 'No files to display'
112118
             return
@@ -171,6 +177,7 @@ contains
171177
                     else
172178
                         call get_dirty_files(files, n_files)
173179
                     end if
180
+                    call mark_incoming_changes(files, n_files)
174181
                     call build_item_list(files, n_files, items, n_items)
175182
                     if (selected > n_items .and. n_items > 0) selected = n_items
176183
                     if (n_items == 0) running = .false.
@@ -184,6 +191,7 @@ contains
184191
                     else
185192
                         call get_dirty_files(files, n_files)
186193
                     end if
194
+                    call mark_incoming_changes(files, n_files)
187195
                     call build_item_list(files, n_files, items, n_items)
188196
                     if (selected > n_items .and. n_items > 0) selected = n_items
189197
                 end if
@@ -195,6 +203,7 @@ contains
195203
                 else
196204
                     call get_dirty_files(files, n_files)
197205
                 end if
206
+                call mark_incoming_changes(files, n_files)
198207
                 call build_item_list(files, n_files, items, n_items)
199208
                 if (selected > n_items .and. n_items > 0) selected = n_items
200209
             case ('s')  ! Show git status (lowercase)
@@ -207,6 +216,33 @@ contains
207216
                 else
208217
                     call get_dirty_files(files, n_files)
209218
                 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)
210246
                 call build_item_list(files, n_files, items, n_items)
211247
                 if (selected > n_items .and. n_items > 0) selected = n_items
212248
             case ('q', 'Q')  ! Quit
@@ -238,11 +274,12 @@ contains
238274
         root%is_staged = .false.
239275
         root%is_unstaged = .false.
240276
         root%is_untracked = .false.
277
+        root%has_incoming = .false.
241278
         root%first_child => null()
242279
         root%next_sibling => null()
243280
 
244281
         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)
246283
         end do
247284
 
248285
         call sort_tree(root)
@@ -291,6 +328,7 @@ contains
291328
             items(n_items)%is_staged = node%is_staged
292329
             items(n_items)%is_unstaged = node%is_unstaged
293330
             items(n_items)%is_untracked = node%is_untracked
331
+            items(n_items)%has_incoming = node%has_incoming
294332
         else
295333
             full_path = ''
296334
         end if
src/git_module.f90modified
@@ -69,6 +69,7 @@ contains
6969
                 temp_files(n_files)%is_untracked = (git_status == '??')
7070
                 temp_files(n_files)%is_staged = (git_status(1:1) /= ' ' .and. git_status(1:1) /= '?')
7171
                 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.
7273
             end if
7374
         end do
7475
 
@@ -142,6 +143,7 @@ contains
142143
                 temp_files(n_files)%is_staged = .false.
143144
                 temp_files(n_files)%is_unstaged = .false.
144145
                 temp_files(n_files)%is_untracked = .false.
146
+                temp_files(n_files)%has_incoming = .false.
145147
                 do i = 1, n_dirty
146148
                     if (trim(dirty_files(i)%path) == trim(line)) then
147149
                         is_dirty_file = .true.
@@ -149,6 +151,7 @@ contains
149151
                         temp_files(n_files)%is_staged = dirty_files(i)%is_staged
150152
                         temp_files(n_files)%is_unstaged = dirty_files(i)%is_unstaged
151153
                         temp_files(n_files)%is_untracked = dirty_files(i)%is_untracked
154
+                        temp_files(n_files)%has_incoming = dirty_files(i)%has_incoming
152155
                         exit
153156
                     end if
154157
                 end do
@@ -223,6 +226,7 @@ contains
223226
                 files(n_files)%is_untracked = (git_status == '??')
224227
                 files(n_files)%is_staged = (git_status(1:1) /= ' ' .and. git_status(1:1) /= '?')
225228
                 files(n_files)%is_unstaged = (git_status(2:2) /= ' ' .and. .not. files(n_files)%is_untracked)
229
+                files(n_files)%has_incoming = .false.
226230
             end if
227231
         end do
228232
 
@@ -402,4 +406,214 @@ contains
402406
         end if
403407
     end subroutine get_repo_info
404408
 
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
+
405619
 end module git_module
src/tree_module.f90modified
@@ -4,10 +4,10 @@ module tree_module
44
 
55
 contains
66
 
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)
88
         type(tree_node), pointer, intent(in) :: node
99
         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
1111
 
1212
         integer :: slash_pos, iostat
1313
         character(len=512) :: first_part, rest
@@ -27,6 +27,7 @@ contains
2727
                     child%is_staged = child%is_staged .or. is_staged
2828
                     child%is_unstaged = child%is_unstaged .or. is_unstaged
2929
                     child%is_untracked = child%is_untracked .or. is_untracked
30
+                    child%has_incoming = child%has_incoming .or. has_incoming
3031
                     return
3132
                 end if
3233
                 if (.not. associated(child%next_sibling)) exit
@@ -51,6 +52,7 @@ contains
5152
             new_child%is_staged = is_staged
5253
             new_child%is_unstaged = is_unstaged
5354
             new_child%is_untracked = is_untracked
55
+            new_child%has_incoming = has_incoming
5456
             new_child%first_child => null()
5557
             new_child%next_sibling => null()
5658
 
@@ -68,7 +70,7 @@ contains
6870
             child => node%first_child
6971
             do while (associated(child))
7072
                 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)
7274
                     return
7375
                 end if
7476
                 if (.not. associated(child%next_sibling)) exit
@@ -82,6 +84,7 @@ contains
8284
             new_child%is_staged = .false.
8385
             new_child%is_unstaged = .false.
8486
             new_child%is_untracked = .false.
87
+            new_child%has_incoming = .false.
8588
             new_child%first_child => null()
8689
             new_child%next_sibling => null()
8790
 
@@ -91,7 +94,7 @@ contains
9194
                 child%next_sibling => new_child
9295
             end if
9396
 
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)
9598
         end if
9699
     end subroutine add_to_tree
97100
 
src/types_module.f90modified
@@ -8,6 +8,7 @@ module types_module
88
         logical :: is_staged
99
         logical :: is_unstaged
1010
         logical :: is_untracked
11
+        logical :: has_incoming
1112
         type(tree_node), pointer :: first_child => null()
1213
         type(tree_node), pointer :: next_sibling => null()
1314
     end type tree_node
@@ -18,6 +19,7 @@ module types_module
1819
         logical :: is_staged
1920
         logical :: is_unstaged
2021
         logical :: is_untracked
22
+        logical :: has_incoming
2123
     end type file_entry
2224
 
2325
     type :: selectable_item
@@ -25,6 +27,7 @@ module types_module
2527
         logical :: is_staged
2628
         logical :: is_unstaged
2729
         logical :: is_untracked
30
+        logical :: has_incoming
2831
         logical :: is_file
2932
     end type selectable_item
3033