fortrangoingonforty/fuss / 36cb077

Browse files

extend branch operations

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
36cb0770aff69bbb3ec4ef1e92bddd32dcfafc72
Parents
3ee1377
Tree
7307e14

3 changed files

StatusFile+-
M src/display_module.f90 1 1
M src/fuss_main.f90 113 0
M src/git_module.f90 243 0
src/display_module.f90modified
@@ -164,7 +164,7 @@ contains
164164
                      achar(27) // '[31m✗' // achar(27) // '[0m=modified ' // &
165165
                      achar(27) // '[90m✗' // achar(27) // '[0m=untracked ' // &
166166
                      achar(27) // '[34m↓' // achar(27) // '[0m=incoming'
167
-        print '(A)', 'Keys: j/k/↑/↓:nav | ←/→:collapse/expand | a:stage | u:unstage | S:stage-all | U:unstage-all | x:discard | b:branch | f:fetch | d:diff | r:delete | l:pull | m:commit | p:push | t:tag | s:status | q:quit'
167
+        print '(A)', 'Keys: j/k/↑/↓:nav | ←/→:collapse/expand | a:stage | u:unstage | S:stage-all | U:unstage-all | x:discard | z:stash | Z:unstash | b:switch | n:new-br | R:del-br | f:fetch | d:diff | r:delete | l:pull | m:commit | p:push | t:tag | s:status | q:quit'
168168
 
169169
         ! Don't free tree - it's owned by interactive_mode
170170
     end subroutine draw_interactive_tree
src/fuss_main.f90modified
@@ -266,6 +266,23 @@ contains
266266
                 if (n_items == 0) running = .false.
267267
                 ! Update branch name display
268268
                 call get_repo_info(repo_name, branch_name)
269
+            case ('n')  ! Create new branch
270
+                call branch_create_prompt()
271
+                ! Refresh files after branch creation
272
+                if (show_all) then
273
+                    call get_all_files(files, n_files)
274
+                else
275
+                    call get_dirty_files(files, n_files)
276
+                end if
277
+                call mark_incoming_changes(files, n_files)
278
+                call build_item_list(files, n_files, items, n_items, tree_root)
279
+                if (selected > n_items .and. n_items > 0) selected = n_items
280
+                if (n_items == 0) running = .false.
281
+                ! Update branch name display
282
+                call get_repo_info(repo_name, branch_name)
283
+            case ('R')  ! Delete branch (Shift+r, since 'r' is used for delete file)
284
+                call branch_delete_prompt()
285
+                ! No need to refresh files or update branch name (stays on current branch)
269286
             case ('f')  ! Git fetch
270287
                 call git_fetch()
271288
                 ! Refresh files after fetch and include files with incoming changes
@@ -325,6 +342,29 @@ contains
325342
                 if (selected > n_items .and. n_items > 0) selected = n_items
326343
                 ! Note: After successful pull, git diff will show no upstream differences
327344
                 ! so has_incoming will be .false. for all files automatically
345
+            case ('z')  ! Stash push (save changes)
346
+                call stash_push_prompt()
347
+                ! Refresh files after stash
348
+                if (show_all) then
349
+                    call get_all_files(files, n_files)
350
+                else
351
+                    call get_dirty_files(files, n_files)
352
+                end if
353
+                call mark_incoming_changes(files, n_files)
354
+                call build_item_list(files, n_files, items, n_items, tree_root)
355
+                if (selected > n_items .and. n_items > 0) selected = n_items
356
+                if (n_items == 0) running = .false.
357
+            case ('Z')  ! Stash pop/apply (restore changes)
358
+                call stash_pop_apply_prompt()
359
+                ! Refresh files after stash pop/apply
360
+                if (show_all) then
361
+                    call get_all_files(files, n_files)
362
+                else
363
+                    call get_dirty_files(files, n_files)
364
+                end if
365
+                call mark_incoming_changes(files, n_files)
366
+                call build_item_list(files, n_files, items, n_items, tree_root)
367
+                if (selected > n_items .and. n_items > 0) selected = n_items
328368
             case ('q', 'Q')  ! Quit
329369
                 running = .false.
330370
             end select
@@ -895,4 +935,77 @@ contains
895935
         call read_key(key)
896936
     end subroutine discard_prompt
897937
 
938
+    subroutine stash_push_prompt()
939
+        character(len=512) :: stash_msg
940
+        logical :: success
941
+        character(len=1) :: key
942
+
943
+        ! Clear screen for stash prompt
944
+        call clear_screen()
945
+        print '(A)', achar(27) // '[1mGit Stash (Save)' // achar(27) // '[0m'
946
+        print '(A)', ''
947
+
948
+        ! Read stash message (optional)
949
+        call read_line('Stash message (optional): ', stash_msg)
950
+
951
+        ! Execute stash push
952
+        call git_stash_push(stash_msg, success)
953
+
954
+        ! Wait for keypress to continue
955
+        call read_key(key)
956
+    end subroutine stash_push_prompt
957
+
958
+    subroutine stash_pop_apply_prompt()
959
+        logical :: success
960
+        character(len=1) :: key
961
+
962
+        ! Clear screen for stash pop/apply prompt
963
+        call clear_screen()
964
+        print '(A)', achar(27) // '[1mGit Stash (Pop/Apply)' // achar(27) // '[0m'
965
+        print '(A)', ''
966
+
967
+        ! Execute stash pop/apply with fzf selection
968
+        call git_stash_pop_apply(success)
969
+
970
+        ! Wait for keypress to continue
971
+        call read_key(key)
972
+    end subroutine stash_pop_apply_prompt
973
+
974
+    subroutine branch_create_prompt()
975
+        character(len=512) :: branch_name
976
+        logical :: success
977
+        character(len=1) :: key
978
+
979
+        ! Clear screen for branch creation
980
+        call clear_screen()
981
+        print '(A)', achar(27) // '[1mCreate New Branch' // achar(27) // '[0m'
982
+        print '(A)', ''
983
+
984
+        ! Read branch name
985
+        call read_line('New branch name: ', branch_name)
986
+
987
+        ! Create branch if name is not empty
988
+        if (len_trim(branch_name) > 0) then
989
+            call git_create_branch(branch_name, success)
990
+            ! Wait for keypress to continue
991
+            call read_key(key)
992
+        end if
993
+    end subroutine branch_create_prompt
994
+
995
+    subroutine branch_delete_prompt()
996
+        logical :: success
997
+        character(len=1) :: key
998
+
999
+        ! Clear screen for branch deletion
1000
+        call clear_screen()
1001
+        print '(A)', achar(27) // '[1mDelete Branch' // achar(27) // '[0m'
1002
+        print '(A)', ''
1003
+
1004
+        ! Call git branch delete with fzf
1005
+        call git_delete_branch(success)
1006
+
1007
+        ! Wait for keypress to continue
1008
+        call read_key(key)
1009
+    end subroutine branch_delete_prompt
1010
+
8981011
 end program fuss
src/git_module.f90modified
@@ -1232,6 +1232,123 @@ contains
12321232
         call execute_command_line('sleep 1', exitstat=status_code)
12331233
     end subroutine git_switch_branch
12341234
 
1235
+    subroutine git_create_branch(branch_name, success)
1236
+        character(len=*), intent(in) :: branch_name
1237
+        logical, intent(out) :: success
1238
+        character(len=1024) :: command
1239
+        integer :: status
1240
+
1241
+        success = .false.
1242
+
1243
+        if (len_trim(branch_name) == 0) then
1244
+            print '(A)', achar(27) // '[33mBranch name cannot be empty' // achar(27) // '[0m'
1245
+            return
1246
+        end if
1247
+
1248
+        ! Create and switch to new branch
1249
+        write(command, '(A,A,A)') 'git switch -c "', trim(branch_name), '" 2>&1'
1250
+        print '(A)', 'Creating new branch...'
1251
+        call execute_command_line(trim(command), exitstat=status)
1252
+
1253
+        if (status == 0) then
1254
+            print '(A)', achar(27) // '[32m✓ Created and switched to branch: ' // trim(branch_name) // achar(27) // '[0m'
1255
+            success = .true.
1256
+        else
1257
+            print '(A)', achar(27) // '[31m✗ Failed to create branch (may already exist)' // achar(27) // '[0m'
1258
+        end if
1259
+
1260
+        call execute_command_line('sleep 1', exitstat=status)
1261
+    end subroutine git_create_branch
1262
+
1263
+    subroutine git_delete_branch(success)
1264
+        use terminal_module, only: read_key
1265
+        logical, intent(out) :: success
1266
+        integer :: status_code
1267
+        character(len=512) :: selected_branch, current_branch
1268
+        character(len=1024) :: command
1269
+        character(len=1) :: key
1270
+
1271
+        success = .false.
1272
+
1273
+        ! Get current branch name to prevent deleting it
1274
+        call execute_command_line('git rev-parse --abbrev-ref HEAD > /tmp/fuss_current_branch.txt 2>&1', &
1275
+                                  exitstat=status_code)
1276
+
1277
+        if (status_code /= 0) then
1278
+            print '(A)', achar(27) // '[31m✗ Could not determine current branch' // achar(27) // '[0m'
1279
+            return
1280
+        end if
1281
+
1282
+        open(unit=99, file='/tmp/fuss_current_branch.txt', status='old', action='read', iostat=status_code)
1283
+        if (status_code == 0) then
1284
+            read(99, '(A)', iostat=status_code) current_branch
1285
+            close(99, status='delete')
1286
+        else
1287
+            return
1288
+        end if
1289
+
1290
+        ! Restore terminal for fzf
1291
+        call execute_command_line('stty sane < /dev/tty', exitstat=status_code)
1292
+
1293
+        ! Use fzf to select branch to delete (exclude current branch)
1294
+        write(command, '(A,A,A)') 'git branch | grep -v "^* " | sed "s/^  //" | grep -v "^', trim(current_branch), &
1295
+                                  '$" | fzf --height=15 --prompt="Delete branch: " > /tmp/fuss_branch_delete.txt'
1296
+        call execute_command_line(trim(command), exitstat=status_code)
1297
+
1298
+        ! Re-enable cbreak mode
1299
+        call execute_command_line('stty cbreak -echo < /dev/tty', exitstat=status_code)
1300
+
1301
+        if (status_code /= 0) then
1302
+            ! User cancelled
1303
+            return
1304
+        end if
1305
+
1306
+        ! Read selected branch
1307
+        open(unit=99, file='/tmp/fuss_branch_delete.txt', status='old', action='read', iostat=status_code)
1308
+        if (status_code /= 0) return
1309
+
1310
+        read(99, '(A)', iostat=status_code) selected_branch
1311
+        close(99, status='delete')
1312
+
1313
+        if (status_code /= 0 .or. len_trim(selected_branch) == 0) return
1314
+
1315
+        ! Confirm deletion
1316
+        print '(A)', ''
1317
+        print '(A)', 'Delete branch "' // trim(selected_branch) // '"?'
1318
+        print '(A)', 'Press ''d'' for regular delete, ''D'' to force delete, any other key to cancel.'
1319
+        call read_key(key)
1320
+
1321
+        if (key == 'd') then
1322
+            ! Regular delete (will fail if not merged)
1323
+            write(command, '(A,A,A)') 'git branch -d "', trim(selected_branch), '" 2>&1'
1324
+            print '(A)', 'Deleting branch...'
1325
+            call execute_command_line(trim(command), exitstat=status_code)
1326
+
1327
+            if (status_code == 0) then
1328
+                print '(A)', achar(27) // '[32m✓ Deleted branch: ' // trim(selected_branch) // achar(27) // '[0m'
1329
+                success = .true.
1330
+            else
1331
+                print '(A)', achar(27) // '[31m✗ Failed to delete (not fully merged? use D to force)' // achar(27) // '[0m'
1332
+            end if
1333
+        else if (key == 'D') then
1334
+            ! Force delete
1335
+            write(command, '(A,A,A)') 'git branch -D "', trim(selected_branch), '" 2>&1'
1336
+            print '(A)', 'Force deleting branch...'
1337
+            call execute_command_line(trim(command), exitstat=status_code)
1338
+
1339
+            if (status_code == 0) then
1340
+                print '(A)', achar(27) // '[32m✓ Force deleted branch: ' // trim(selected_branch) // achar(27) // '[0m'
1341
+                success = .true.
1342
+            else
1343
+                print '(A)', achar(27) // '[31m✗ Failed to delete branch' // achar(27) // '[0m'
1344
+            end if
1345
+        else
1346
+            print '(A)', 'Delete cancelled.'
1347
+        end if
1348
+
1349
+        call execute_command_line('sleep 1', exitstat=status_code)
1350
+    end subroutine git_delete_branch
1351
+
12351352
     subroutine git_push_tag(tag_name, success)
12361353
         character(len=*), intent(in) :: tag_name
12371354
         logical, intent(out) :: success
@@ -1253,4 +1370,130 @@ contains
12531370
         end if
12541371
     end subroutine git_push_tag
12551372
 
1373
+    subroutine git_stash_push(stash_message, success)
1374
+        character(len=*), intent(in) :: stash_message
1375
+        logical, intent(out) :: success
1376
+        character(len=2048) :: command
1377
+        integer :: status
1378
+
1379
+        success = .false.
1380
+
1381
+        ! Build stash push command with optional message
1382
+        if (len_trim(stash_message) > 0) then
1383
+            write(command, '(A,A,A)') 'git stash push -m "', trim(stash_message), '" 2>&1'
1384
+        else
1385
+            command = 'git stash push 2>&1'
1386
+        end if
1387
+
1388
+        print '(A)', 'Stashing changes...'
1389
+        call execute_command_line(trim(command), exitstat=status)
1390
+
1391
+        if (status == 0) then
1392
+            print '(A)', achar(27) // '[32m✓ Changes stashed successfully!' // achar(27) // '[0m'
1393
+            success = .true.
1394
+        else
1395
+            print '(A)', achar(27) // '[31m✗ Stash failed (no changes to stash?)' // achar(27) // '[0m'
1396
+        end if
1397
+
1398
+        print '(A)', 'Press any key to continue...'
1399
+    end subroutine git_stash_push
1400
+
1401
+    subroutine git_stash_pop_apply(success)
1402
+        use terminal_module, only: read_key
1403
+        logical, intent(out) :: success
1404
+        integer :: status_code
1405
+        character(len=512) :: selected_stash
1406
+        character(len=1024) :: command
1407
+        character(len=1) :: key
1408
+
1409
+        success = .false.
1410
+
1411
+        ! Check if there are any stashes
1412
+        call execute_command_line('git stash list > /tmp/fuss_stash_check.txt 2>&1', exitstat=status_code)
1413
+        if (status_code /= 0) then
1414
+            print '(A)', achar(27) // '[33mNo stashes available' // achar(27) // '[0m'
1415
+            print '(A)', 'Press any key to continue...'
1416
+            return
1417
+        end if
1418
+
1419
+        ! Check if stash list is empty
1420
+        call execute_command_line('test -s /tmp/fuss_stash_check.txt', exitstat=status_code)
1421
+        if (status_code /= 0) then
1422
+            print '(A)', achar(27) // '[33mNo stashes available' // achar(27) // '[0m'
1423
+            print '(A)', 'Press any key to continue...'
1424
+            call execute_command_line('rm -f /tmp/fuss_stash_check.txt', exitstat=status_code)
1425
+            return
1426
+        end if
1427
+
1428
+        call execute_command_line('rm -f /tmp/fuss_stash_check.txt', exitstat=status_code)
1429
+
1430
+        ! Restore terminal for fzf
1431
+        call execute_command_line('stty sane < /dev/tty', exitstat=status_code)
1432
+
1433
+        ! Use fzf to select stash with preview
1434
+        call execute_command_line('git stash list --format="%gd: %s" | ' // &
1435
+                                  'fzf --height=15 --prompt="Select stash: " ' // &
1436
+                                  '--preview="git stash show -p {1}" --preview-window=right:50% ' // &
1437
+                                  '> /tmp/fuss_stash_select.txt', &
1438
+                                  exitstat=status_code)
1439
+
1440
+        ! Re-enable cbreak mode
1441
+        call execute_command_line('stty cbreak -echo < /dev/tty', exitstat=status_code)
1442
+
1443
+        if (status_code /= 0) then
1444
+            ! User cancelled
1445
+            return
1446
+        end if
1447
+
1448
+        ! Read selected stash
1449
+        open(unit=99, file='/tmp/fuss_stash_select.txt', status='old', action='read', iostat=status_code)
1450
+        if (status_code /= 0) return
1451
+
1452
+        read(99, '(A)', iostat=status_code) selected_stash
1453
+        close(99, status='delete')
1454
+
1455
+        if (status_code /= 0 .or. len_trim(selected_stash) == 0) return
1456
+
1457
+        ! Extract stash reference (e.g., "stash@{0}")
1458
+        ! Format is "stash@{N}: message", so we take everything before the first ":"
1459
+        command = selected_stash(1:index(selected_stash, ':') - 1)
1460
+
1461
+        ! Ask user whether to pop or apply
1462
+        print '(A)', ''
1463
+        print '(A)', 'Pop (apply and remove) or Apply (keep stash)?'
1464
+        print '(A)', 'Press ''p'' to pop, ''a'' to apply, any other key to cancel.'
1465
+        call read_key(key)
1466
+
1467
+        if (key == 'p' .or. key == 'P') then
1468
+            ! Pop the stash (apply and remove)
1469
+            print '(A)', 'Popping stash...'
1470
+            write(command, '(A,A,A)') 'git stash pop "', trim(command), '" 2>&1'
1471
+            call execute_command_line(trim(command), exitstat=status_code)
1472
+
1473
+            if (status_code == 0) then
1474
+                print '(A)', achar(27) // '[32m✓ Stash popped successfully!' // achar(27) // '[0m'
1475
+                success = .true.
1476
+            else
1477
+                print '(A)', achar(27) // '[31m✗ Stash pop failed (conflicts?)' // achar(27) // '[0m'
1478
+            end if
1479
+        else if (key == 'a' .or. key == 'A') then
1480
+            ! Apply the stash (keep it)
1481
+            print '(A)', 'Applying stash...'
1482
+            write(command, '(A,A,A)') 'git stash apply "', trim(command), '" 2>&1'
1483
+            call execute_command_line(trim(command), exitstat=status_code)
1484
+
1485
+            if (status_code == 0) then
1486
+                print '(A)', achar(27) // '[32m✓ Stash applied successfully!' // achar(27) // '[0m'
1487
+                success = .true.
1488
+            else
1489
+                print '(A)', achar(27) // '[31m✗ Stash apply failed (conflicts?)' // achar(27) // '[0m'
1490
+            end if
1491
+        else
1492
+            print '(A)', 'Stash operation cancelled.'
1493
+        end if
1494
+
1495
+        print '(A)', ''
1496
+        print '(A)', 'Press any key to continue...'
1497
+    end subroutine git_stash_pop_apply
1498
+
12561499
 end module git_module