giterm 2.0.2 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/giterm +125 -42
  3. metadata +4 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ff35951790433a2f8aee112a5f1da5753a64076def68327e1a2cf02bd99c0e18
4
- data.tar.gz: fc8c53303b5b678b21e99b6312f3d93587f83ddb0c809dc51069f9c0b16bc883
3
+ metadata.gz: a01a43a0c33b199d4c4295839aeaa21accde4c8e9dee216c635ecc7835176e53
4
+ data.tar.gz: 1f0ac3d3d407e7e25e1e13513b67fff172b84d82d3ca15a70067941c4b685128
5
5
  SHA512:
6
- metadata.gz: b4a1045fe19fbafe0f26a99a0da543d6c0c723d49189ac76fae28c941cd6c357bd8929c1a1e3b6b6f561f58d7fcf6129c82093b6be952467addf6759a8b7ff10
7
- data.tar.gz: 333992ec11e581dd6821430a30250c32113a58480bb752bb81988296cf9578840e9940be8041e393a39ae1c83403e2ef91cfa37db14855b1b01d8d4354b25211
6
+ metadata.gz: 5c421275ade0738dbe055be7e0b2581471610111d125dfee77e99fb8a6a711e8416f5cf2d225a7fda1b745ebf2c41dc0c23159d37f861d2fcd09f29f20e0f1da
7
+ data.tar.gz: 003f4737089d19deecbac244b25201ca5fcacb97642c64abdb0f100c9b97c3abbe6519dd201f4e15e9940e0448d02f075419085f6499a1d81661ed2054c1b128
data/giterm CHANGED
@@ -7,7 +7,7 @@
7
7
  # Author: Geir Isene <g@isene.com> (adapted from RTFM)
8
8
  # Github: https://github.com/isene/GiTerm
9
9
  # License: Public domain
10
- @version = '2.0.1'
10
+ @version = '2.0.4'
11
11
 
12
12
  # SAVE & STORE TERMINAL {{{1
13
13
  ORIG_STTY = `stty -g 2>/dev/null`.chomp rescue ''
@@ -911,7 +911,26 @@ def github_request_http(endpoint)
911
911
 
912
912
  JSON.parse(body)
913
913
  else
914
- { error: "GitHub API error: #{response.code} - #{response.message}" }
914
+ error_msg = case response.code
915
+ when '401'
916
+ 'Authentication failed (token expired or invalid). Press T to update token.'
917
+ when '403'
918
+ remaining = response['X-RateLimit-Remaining']
919
+ if remaining == '0'
920
+ reset_time = response['X-RateLimit-Reset']
921
+ reset_in = reset_time ? ((reset_time.to_i - Time.now.to_i) / 60.0).ceil : '?'
922
+ "GitHub API rate limit exceeded. Resets in ~#{reset_in} minutes."
923
+ else
924
+ "Access forbidden (403). Check token permissions."
925
+ end
926
+ when '404'
927
+ 'Resource not found (404). Repository may be private or deleted.'
928
+ when '429'
929
+ 'Too many requests (429). Please wait a moment and retry.'
930
+ else
931
+ "GitHub API error: #{response.code} - #{response.message}"
932
+ end
933
+ { error: error_msg }
915
934
  end
916
935
  end
917
936
  rescue JSON::ParserError => e
@@ -931,33 +950,62 @@ end
931
950
 
932
951
  def github_request_curl(endpoint)
933
952
  begin
934
- # Build curl command as a string to ensure proper formatting
953
+ # Build curl command with separate connect and total timeouts
935
954
  url = "https://api.github.com#{endpoint}"
936
- curl_cmd = "curl -s -f --max-time 10 " \
955
+ curl_cmd = "curl -s --connect-timeout 10 --max-time 20 " \
956
+ "-w '\\n%{http_code}' " \
937
957
  "-H 'Authorization: token #{@github_token}' " \
938
958
  "-H 'Accept: application/vnd.github.v3+json' " \
939
959
  "-H 'User-Agent: GiTerm/1.0' " \
940
960
  "'#{url}'"
941
-
961
+
942
962
  # Debug: log the curl command (without showing full token)
943
963
  debug_cmd = curl_cmd.sub(@github_token, "#{@github_token[0..10]}...")
944
964
  log_debug("Executing curl: #{debug_cmd}")
945
-
965
+
946
966
  # Capture both stdout and stderr for better debugging
947
- result = `#{curl_cmd} 2>&1`
967
+ raw_result = `#{curl_cmd} 2>&1`
948
968
  exit_code = $?.exitstatus
949
-
950
- if exit_code == 0 && !result.empty?
951
- JSON.parse(result)
969
+
970
+ if exit_code == 0
971
+ # Extract HTTP status code from the last line (-w appends it)
972
+ lines = raw_result.rstrip.split("\n")
973
+ http_code = lines.last.strip
974
+ body = lines[0...-1].join("\n")
975
+
976
+ if http_code == '200' && !body.empty?
977
+ JSON.parse(body)
978
+ elsif body.empty?
979
+ { error: 'Empty response body from GitHub API.' }
980
+ else
981
+ error_msg = case http_code
982
+ when '401'
983
+ 'Authentication failed (token expired or invalid). Press T to update token.'
984
+ when '403'
985
+ parsed = JSON.parse(body) rescue {}
986
+ if parsed['message']&.include?('rate limit')
987
+ 'GitHub API rate limit exceeded. Wait a few minutes and retry.'
988
+ else
989
+ "Access forbidden (403). Check token permissions."
990
+ end
991
+ when '404'
992
+ 'Resource not found (404). Repository may be private or deleted.'
993
+ when '429'
994
+ 'Too many requests (429). Please wait a moment and retry.'
995
+ else
996
+ "GitHub API error (HTTP #{http_code})."
997
+ end
998
+ { error: error_msg }
999
+ end
952
1000
  else
953
1001
  error_msg = case exit_code
954
- when 22 then 'HTTP error (likely 401/403 - check token)'
955
- when 6 then 'Could not resolve host (network issue)'
956
- when 7 then 'Failed to connect (network issue)'
957
- when 28 then 'Operation timeout'
958
- else "curl failed (exit code: #{exit_code})"
1002
+ when 6 then 'Could not resolve host. Check network connection.'
1003
+ when 7 then 'Failed to connect to GitHub. Check network connection.'
1004
+ when 28 then 'Request timed out. GitHub may be slow or unreachable.'
1005
+ when 35 then 'SSL/TLS connection error.'
1006
+ else "Network error (curl exit code: #{exit_code})."
959
1007
  end
960
- { error: "#{error_msg}. Response: #{result[0..100]}" }
1008
+ { error: error_msg }
961
1009
  end
962
1010
  rescue JSON::ParserError => e
963
1011
  { error: "Invalid JSON from curl: #{e.message}" }
@@ -1225,17 +1273,40 @@ rescue => e
1225
1273
  log_debug("Error in extended fetch: #{e.message}")
1226
1274
  end
1227
1275
 
1276
+ def filter_markdown_clutter(text)
1277
+ # Remove badge markdown (both simple and nested)
1278
+ # Matches: ![text](url) and [![text](url)](link)
1279
+ text = text.gsub(/!\[(?:[^\]]*\[[^\]]*\][^\]]*|[^\]]*)\]\([^\)]+\)/, '')
1280
+
1281
+ # Remove HTML image tags
1282
+ text = text.gsub(/<img[^>]*>/, '')
1283
+
1284
+ # Remove HTML break tags
1285
+ text = text.gsub(/<br[^>]*>/, '')
1286
+
1287
+ # Remove lines that are only badges/links (leftover clutter)
1288
+ text = text.lines.reject { |line| line.strip.match?(/^[\[\]():\/.a-zA-Z0-9_-]+$/) && line.include?('](') }.join
1289
+
1290
+ # Remove excessive blank lines (more than 2 consecutive)
1291
+ text = text.gsub(/\n{3,}/, "\n\n")
1292
+
1293
+ # Remove lines that are only whitespace
1294
+ text = text.lines.reject { |line| line.strip.empty? && line != "\n" }.join
1295
+
1296
+ text.strip
1297
+ end
1298
+
1228
1299
  def fetch_readme(repo_full_name)
1229
1300
  return nil unless repo_full_name
1230
-
1301
+
1231
1302
  # Try different README variations
1232
1303
  readme_files = ['README.md', 'readme.md', 'README.txt', 'README', 'readme.txt']
1233
-
1304
+
1234
1305
  readme_files.each do |filename|
1235
1306
  begin
1236
1307
  result = github_request("/repos/#{repo_full_name}/contents/#{filename}")
1237
1308
  next if result.is_a?(Hash) && result[:error]
1238
-
1309
+
1239
1310
  # Decode base64 content
1240
1311
  if result.is_a?(Hash) && result['content'] && result['encoding'] == 'base64'
1241
1312
  decoded = Base64.decode64(result['content'].gsub(/\s/, ''))
@@ -1244,15 +1315,16 @@ def fetch_readme(repo_full_name)
1244
1315
  unless decoded.valid_encoding?
1245
1316
  decoded = decoded.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '?')
1246
1317
  end
1247
- # Limit README display to avoid overwhelming the pane
1248
- return decoded.lines.first(20).join.strip
1318
+ # Filter markdown clutter and limit README display
1319
+ filtered = filter_markdown_clutter(decoded)
1320
+ return filtered.lines.first(20).join.strip
1249
1321
  end
1250
1322
  rescue => e
1251
1323
  log_debug("Error fetching README #{filename}: #{e.message}")
1252
1324
  next
1253
1325
  end
1254
1326
  end
1255
-
1327
+
1256
1328
  nil
1257
1329
  end
1258
1330
 
@@ -1282,20 +1354,19 @@ def fetch_repo_files(repo_full_name, path = '')
1282
1354
  # Show directories first, then files
1283
1355
  (dirs + files).each do |item|
1284
1356
  icon = case item['type']
1285
- when 'dir' then '📁'
1357
+ when 'dir' then ''
1286
1358
  when 'file'
1287
1359
  case File.extname(item['name']).downcase
1288
- when '.md' then '📄'
1289
- when '.rb' then '💎'
1290
- when '.py' then '🐍'
1291
- when '.js' then '📜'
1292
- when '.json' then '⚙️'
1293
- when '.yml', '.yaml' then '⚙️'
1294
- else '📄'
1360
+ when '.md' then ''
1361
+ when '.rb' then ''
1362
+ when '.py' then ''
1363
+ when '.js' then ''
1364
+ when '.json', '.yml', '.yaml' then ''
1365
+ else ''
1295
1366
  end
1296
- else ''
1367
+ else '?'
1297
1368
  end
1298
-
1369
+
1299
1370
  size_info = item['size'] ? " (#{format_file_size(item['size'])})" : ""
1300
1371
  content += "#{icon} #{item['name']}#{size_info}\n"
1301
1372
  end
@@ -1325,15 +1396,21 @@ def github_issues
1325
1396
  return if @selected_repo.empty?
1326
1397
 
1327
1398
  result = github_request("/repos/#{@selected_repo}/issues?state=open&per_page=50")
1328
-
1329
- if result[:error]
1399
+
1400
+ if result.is_a?(Hash) && result[:error]
1330
1401
  @p_left.say(result[:error].fg(196))
1402
+ @p_bottom.say('Failed to load issues.')
1331
1403
  return
1332
1404
  end
1333
-
1405
+
1406
+ unless result.is_a?(Array)
1407
+ @p_left.say('Unexpected response format from GitHub API.'.fg(196))
1408
+ return
1409
+ end
1410
+
1334
1411
  result.each do |issue|
1335
1412
  next if issue['pull_request'] # Skip PRs
1336
-
1413
+
1337
1414
  @github_issues << {
1338
1415
  number: issue['number'],
1339
1416
  title: issue['title'],
@@ -1412,12 +1489,18 @@ def github_pull_requests
1412
1489
  return if @selected_repo.empty?
1413
1490
 
1414
1491
  result = github_request("/repos/#{@selected_repo}/pulls?state=open&per_page=50")
1415
-
1416
- if result[:error]
1492
+
1493
+ if result.is_a?(Hash) && result[:error]
1417
1494
  @p_left.say(result[:error].fg(196))
1495
+ @p_bottom.say('Failed to load pull requests.')
1418
1496
  return
1419
1497
  end
1420
-
1498
+
1499
+ unless result.is_a?(Array)
1500
+ @p_left.say('Unexpected response format from GitHub API.'.fg(196))
1501
+ return
1502
+ end
1503
+
1421
1504
  result.each do |pr|
1422
1505
  @github_prs << {
1423
1506
  number: pr['number'],
@@ -1534,14 +1617,14 @@ def github_search_repositories
1534
1617
  # Perform the search
1535
1618
  result = github_request("/search/repositories?q=#{CGI.escape(query)}&sort=stars&order=desc&per_page=50")
1536
1619
 
1537
- if result[:error]
1620
+ if result.is_a?(Hash) && result[:error]
1538
1621
  @p_left.clear
1539
1622
  @p_left.say("Search Error: #{result[:error]}".fg(196))
1540
1623
  return
1541
1624
  end
1542
-
1625
+
1543
1626
  # Process search results
1544
- if result['items'] && result['items'].any?
1627
+ if result.is_a?(Hash) && result['items'] && result['items'].any?
1545
1628
  result['items'].each do |repo|
1546
1629
  @github_search_results << {
1547
1630
  id: repo['id'],
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: giterm
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.2
4
+ version: 2.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Geir Isene
8
8
  autorequire:
9
9
  bindir: "."
10
10
  cert_chain: []
11
- date: 2025-08-19 00:00:00.000000000 Z
11
+ date: 2026-03-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rcurses
@@ -54,7 +54,8 @@ dependencies:
54
54
  version: '13.0'
55
55
  description: 'GiTerm is a powerful terminal interface for Git and GitHub, providing
56
56
  an intuitive TUI for repository management, issue tracking, and pull request handling.
57
- Version 2.0.2: Fixed macOS branch selection and commit diff display issues.'
57
+ Version 2.0.4: Improved GitHub API error recovery with specific messages for auth,
58
+ rate-limit, and network failures.'
58
59
  email:
59
60
  - g@isene.com
60
61
  executables: