visidata 2.11.dev0__py3-none-any.whl → 3.0__py3-none-any.whl

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 (253) hide show
  1. visidata/__init__.py +72 -91
  2. visidata/_input.py +263 -44
  3. visidata/_open.py +84 -29
  4. visidata/_types.py +22 -4
  5. visidata/_urlcache.py +17 -4
  6. visidata/aggregators.py +65 -25
  7. visidata/apps/__init__.py +0 -0
  8. visidata/apps/vdsql/__about__.py +8 -0
  9. visidata/apps/vdsql/__init__.py +5 -0
  10. visidata/apps/vdsql/__main__.py +27 -0
  11. visidata/apps/vdsql/_ibis.py +748 -0
  12. visidata/apps/vdsql/bigquery.py +61 -0
  13. visidata/apps/vdsql/clickhouse.py +53 -0
  14. visidata/apps/vdsql/setup.py +40 -0
  15. visidata/apps/vdsql/snowflake.py +67 -0
  16. visidata/apps/vgit/__init__.py +13 -0
  17. visidata/apps/vgit/__main__.py +3 -0
  18. visidata/apps/vgit/abort.py +23 -0
  19. visidata/apps/vgit/blame.py +76 -0
  20. visidata/apps/vgit/branch.py +153 -0
  21. visidata/apps/vgit/config.py +95 -0
  22. visidata/apps/vgit/diff.py +169 -0
  23. visidata/apps/vgit/gitsheet.py +161 -0
  24. visidata/apps/vgit/grep.py +37 -0
  25. visidata/apps/vgit/log.py +81 -0
  26. visidata/apps/vgit/main.py +55 -0
  27. visidata/apps/vgit/remote.py +57 -0
  28. visidata/apps/vgit/repos.py +71 -0
  29. visidata/apps/vgit/setup.py +37 -0
  30. visidata/apps/vgit/stash.py +69 -0
  31. visidata/apps/vgit/status.py +204 -0
  32. visidata/apps/vgit/statusbar.py +34 -0
  33. visidata/basesheet.py +59 -50
  34. visidata/canvas.py +251 -99
  35. visidata/choose.py +15 -11
  36. visidata/clean_names.py +29 -0
  37. visidata/clipboard.py +84 -18
  38. visidata/cliptext.py +220 -46
  39. visidata/cmdlog.py +89 -114
  40. visidata/color.py +142 -56
  41. visidata/column.py +134 -131
  42. visidata/ddw/input.ddw +74 -79
  43. visidata/ddw/regex.ddw +57 -0
  44. visidata/ddwplay.py +33 -14
  45. visidata/deprecated.py +77 -3
  46. visidata/desktop/visidata.desktop +7 -0
  47. visidata/editor.py +12 -6
  48. visidata/errors.py +5 -1
  49. visidata/experimental/__init__.py +0 -0
  50. visidata/experimental/diff_sheet.py +29 -0
  51. visidata/experimental/digit_autoedit.py +6 -0
  52. visidata/experimental/gdrive.py +89 -0
  53. visidata/experimental/google.py +37 -0
  54. visidata/experimental/gsheets.py +79 -0
  55. visidata/experimental/live_search.py +37 -0
  56. visidata/experimental/liveupdate.py +45 -0
  57. visidata/experimental/mark.py +133 -0
  58. visidata/experimental/noahs_tapestry/__init__.py +1 -0
  59. visidata/experimental/noahs_tapestry/tapestry.py +147 -0
  60. visidata/experimental/rownum.py +73 -0
  61. visidata/experimental/slide_cells.py +26 -0
  62. visidata/expr.py +8 -4
  63. visidata/extensible.py +32 -6
  64. visidata/features/__init__.py +0 -0
  65. visidata/features/addcol_audiometadata.py +42 -0
  66. visidata/features/addcol_histogram.py +34 -0
  67. visidata/features/canvas_save_svg.py +69 -0
  68. visidata/features/change_precision.py +46 -0
  69. visidata/features/cmdpalette.py +163 -0
  70. visidata/features/colorbrewer.py +363 -0
  71. visidata/{colorsheet.py → features/colorsheet.py} +17 -16
  72. visidata/features/command_server.py +105 -0
  73. visidata/features/currency_to_usd.py +70 -0
  74. visidata/{customdate.py → features/customdate.py} +2 -0
  75. visidata/features/dedupe.py +132 -0
  76. visidata/{describe.py → features/describe.py} +17 -15
  77. visidata/features/errors_guide.py +26 -0
  78. visidata/features/expand_cols.py +202 -0
  79. visidata/{fill.py → features/fill.py} +4 -2
  80. visidata/{freeze.py → features/freeze.py} +11 -6
  81. visidata/features/graph_seaborn.py +79 -0
  82. visidata/features/helloworld.py +10 -0
  83. visidata/features/hint_types.py +17 -0
  84. visidata/{incr.py → features/incr.py} +5 -0
  85. visidata/{join.py → features/join.py} +107 -53
  86. visidata/features/known_cols.py +21 -0
  87. visidata/features/layout.py +62 -0
  88. visidata/{melt.py → features/melt.py} +33 -21
  89. visidata/features/normcol.py +118 -0
  90. visidata/features/open_config.py +7 -0
  91. visidata/features/open_syspaste.py +18 -0
  92. visidata/features/ping.py +157 -0
  93. visidata/features/procmgr.py +208 -0
  94. visidata/features/random_sample.py +6 -0
  95. visidata/{regex.py → features/regex.py} +47 -31
  96. visidata/features/reload_every.py +55 -0
  97. visidata/features/rename_col_cascade.py +30 -0
  98. visidata/features/scroll_context.py +60 -0
  99. visidata/features/select_equal_selected.py +11 -0
  100. visidata/features/setcol_fake.py +65 -0
  101. visidata/{slide.py → features/slide.py} +75 -21
  102. visidata/features/sparkline.py +48 -0
  103. visidata/features/status_source.py +20 -0
  104. visidata/{sysedit.py → features/sysedit.py} +2 -1
  105. visidata/features/sysopen_mailcap.py +46 -0
  106. visidata/features/term_extras.py +13 -0
  107. visidata/{transpose.py → features/transpose.py} +5 -4
  108. visidata/features/type_ipaddr.py +73 -0
  109. visidata/features/type_url.py +11 -0
  110. visidata/{unfurl.py → features/unfurl.py} +9 -9
  111. visidata/{window.py → features/window.py} +2 -2
  112. visidata/form.py +50 -21
  113. visidata/freqtbl.py +81 -33
  114. visidata/fuzzymatch.py +414 -0
  115. visidata/graph.py +105 -33
  116. visidata/guide.py +180 -0
  117. visidata/help.py +75 -44
  118. visidata/hint.py +39 -0
  119. visidata/indexsheet.py +109 -0
  120. visidata/input_history.py +55 -0
  121. visidata/interface.py +58 -0
  122. visidata/keys.py +17 -16
  123. visidata/loaders/__init__.py +9 -0
  124. visidata/loaders/_pandas.py +61 -21
  125. visidata/loaders/api_airtable.py +70 -0
  126. visidata/loaders/api_bitio.py +102 -0
  127. visidata/loaders/api_matrix.py +148 -0
  128. visidata/loaders/api_reddit.py +306 -0
  129. visidata/loaders/api_zulip.py +249 -0
  130. visidata/loaders/archive.py +41 -7
  131. visidata/loaders/arrow.py +7 -7
  132. visidata/loaders/conll.py +49 -0
  133. visidata/loaders/csv.py +25 -7
  134. visidata/loaders/eml.py +3 -4
  135. visidata/loaders/f5log.py +1204 -0
  136. visidata/loaders/fec.py +325 -0
  137. visidata/loaders/fixed_width.py +3 -5
  138. visidata/loaders/frictionless.py +3 -3
  139. visidata/loaders/geojson.py +8 -5
  140. visidata/loaders/google.py +48 -0
  141. visidata/loaders/graphviz.py +4 -4
  142. visidata/loaders/hdf5.py +4 -4
  143. visidata/loaders/html.py +48 -10
  144. visidata/loaders/http.py +84 -30
  145. visidata/loaders/imap.py +20 -10
  146. visidata/loaders/jrnl.py +52 -0
  147. visidata/loaders/json.py +83 -29
  148. visidata/loaders/jsonla.py +74 -0
  149. visidata/loaders/lsv.py +15 -11
  150. visidata/loaders/mailbox.py +40 -0
  151. visidata/loaders/markdown.py +1 -3
  152. visidata/loaders/mbtiles.py +4 -5
  153. visidata/loaders/mysql.py +11 -13
  154. visidata/loaders/npy.py +7 -7
  155. visidata/loaders/odf.py +4 -1
  156. visidata/loaders/orgmode.py +428 -0
  157. visidata/loaders/pandas_freqtbl.py +14 -20
  158. visidata/loaders/parquet.py +62 -6
  159. visidata/loaders/pcap.py +3 -3
  160. visidata/loaders/pdf.py +4 -3
  161. visidata/loaders/png.py +19 -13
  162. visidata/loaders/postgres.py +9 -8
  163. visidata/loaders/rec.py +7 -3
  164. visidata/loaders/s3.py +342 -0
  165. visidata/loaders/sas.py +5 -5
  166. visidata/loaders/scrape.py +186 -0
  167. visidata/loaders/shp.py +6 -5
  168. visidata/loaders/spss.py +5 -6
  169. visidata/loaders/sqlite.py +68 -28
  170. visidata/loaders/texttables.py +1 -1
  171. visidata/loaders/toml.py +60 -0
  172. visidata/loaders/tsv.py +61 -19
  173. visidata/loaders/ttf.py +19 -7
  174. visidata/loaders/unzip_http.py +6 -5
  175. visidata/loaders/usv.py +1 -1
  176. visidata/loaders/vcf.py +16 -16
  177. visidata/loaders/vds.py +10 -7
  178. visidata/loaders/vdx.py +30 -5
  179. visidata/loaders/xlsb.py +8 -1
  180. visidata/loaders/xlsx.py +145 -25
  181. visidata/loaders/xml.py +6 -3
  182. visidata/loaders/xword.py +4 -4
  183. visidata/loaders/yaml.py +15 -5
  184. visidata/macos.py +1 -1
  185. visidata/macros.py +130 -41
  186. visidata/main.py +119 -94
  187. visidata/mainloop.py +101 -154
  188. visidata/man/parse_options.py +2 -2
  189. visidata/man/vd.1 +302 -147
  190. visidata/man/vd.txt +291 -151
  191. visidata/memory.py +3 -3
  192. visidata/menu.py +104 -423
  193. visidata/metasheets.py +59 -141
  194. visidata/modify.py +79 -23
  195. visidata/motd.py +3 -3
  196. visidata/mouse.py +137 -0
  197. visidata/movement.py +43 -35
  198. visidata/optionssheet.py +99 -0
  199. visidata/path.py +131 -43
  200. visidata/pivot.py +74 -47
  201. visidata/plugins.py +65 -192
  202. visidata/pyobj.py +50 -201
  203. visidata/rename_col.py +20 -0
  204. visidata/save.py +42 -20
  205. visidata/search.py +54 -10
  206. visidata/selection.py +84 -5
  207. visidata/settings.py +162 -24
  208. visidata/sheets.py +229 -257
  209. visidata/shell.py +51 -21
  210. visidata/sidebar.py +162 -0
  211. visidata/sort.py +11 -4
  212. visidata/statusbar.py +113 -104
  213. visidata/stored_list.py +43 -0
  214. visidata/stored_prop.py +38 -0
  215. visidata/tests/conftest.py +3 -3
  216. visidata/tests/test_cliptext.py +39 -0
  217. visidata/tests/test_commands.py +62 -7
  218. visidata/tests/test_edittext.py +2 -2
  219. visidata/tests/test_features.py +17 -0
  220. visidata/tests/test_menu.py +14 -0
  221. visidata/tests/test_path.py +13 -4
  222. visidata/text_source.py +53 -0
  223. visidata/textsheet.py +10 -3
  224. visidata/theme.py +44 -0
  225. visidata/themes/__init__.py +0 -0
  226. visidata/themes/ascii8.py +84 -0
  227. visidata/themes/asciimono.py +84 -0
  228. visidata/themes/light.py +17 -0
  229. visidata/threads.py +87 -39
  230. visidata/tuiwin.py +22 -0
  231. visidata/type_currency.py +22 -3
  232. visidata/type_date.py +31 -9
  233. visidata/type_floatsi.py +5 -1
  234. visidata/undo.py +18 -6
  235. visidata/utils.py +106 -23
  236. visidata/vdobj.py +28 -17
  237. visidata/windows.py +10 -0
  238. visidata/wrappers.py +9 -3
  239. visidata-3.0.data/data/share/applications/visidata.desktop +7 -0
  240. {visidata-2.11.dev0.data → visidata-3.0.data}/data/share/man/man1/vd.1 +302 -147
  241. {visidata-2.11.dev0.data → visidata-3.0.data}/data/share/man/man1/visidata.1 +302 -147
  242. visidata-3.0.data/scripts/vd2to3.vdx +9 -0
  243. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/METADATA +13 -11
  244. visidata-3.0.dist-info/RECORD +257 -0
  245. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/WHEEL +1 -1
  246. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/entry_points.txt +0 -1
  247. visidata/layout.py +0 -44
  248. visidata/misc.py +0 -5
  249. visidata-2.11.dev0.dist-info/RECORD +0 -142
  250. /visidata/{repeat.py → features/repeat.py} +0 -0
  251. {visidata-2.11.dev0.data → visidata-3.0.data}/scripts/vd +0 -0
  252. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/LICENSE.gpl3 +0 -0
  253. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,169 @@
1
+ from visidata import vd, VisiData, ItemColumn, RowColorizer, AttrDict, Column
2
+
3
+ from .gitsheet import GitSheet
4
+
5
+ vd.option('git_diff_algo', 'minimal', 'algorithm to use for git diff')
6
+ vd.theme_option('color_git_hunk_add', 'green', 'color for added hunk lines')
7
+ vd.theme_option('color_git_hunk_del', 'red', 'color for deleted hunk lines')
8
+ vd.theme_option('color_git_hunk_diff', 'yellow', 'color for hunk diffs')
9
+
10
+ @VisiData.api
11
+ def git_diff(vd, p, args):
12
+ return GitDiffSheet('git-diff', source=p, gitargs=args)
13
+
14
+ def _parseStartCount(s):
15
+ sc = s.split(',')
16
+ if len(sc) == 2:
17
+ return sc
18
+ if len(sc) == 1:
19
+ return sc[0], 1
20
+
21
+
22
+ class GitDiffSheet(GitSheet):
23
+ columns = [
24
+ ItemColumn('a_fn', width=0),
25
+ ItemColumn('fn', 'b_fn', width=30, hoffset=-28),
26
+ ItemColumn('a_lineno', type=int, width=0),
27
+ ItemColumn('lineno', 'b_lineno', type=int, width=8),
28
+ Column('count', width=10, getter=lambda c,r: c.sheet.hunkCount(r)),
29
+ ItemColumn('context'),
30
+ ItemColumn('lines', type=''.join),
31
+ ]
32
+
33
+ guide = '''# {sheet.cursorRow.a_fn}
34
+ {sheet.cursorLines}'''
35
+
36
+ def hunkCount(self, row):
37
+ return f'-{row.a_count}/+{row.b_count}'
38
+
39
+ @property
40
+ def cursorLines(self):
41
+ r = ''
42
+ for line in self.cursorRow.lines[2:]:
43
+ if line.startswith('-'):
44
+ line = '[:git_hunk_del]' + line + '[/]'
45
+ elif line.startswith('+'):
46
+ line = '[:git_hunk_add]' + line + '[/]'
47
+
48
+ r += line + '\n'
49
+ r = r[4:]
50
+ return r
51
+
52
+ def iterload(self):
53
+ current_hunk = None
54
+
55
+ for line in self.git_lines('diff --patch --inter-hunk-context=2 --find-renames --no-color --no-prefix', *self.gitargs):
56
+ if line.startswith('diff'):
57
+ diff_started = True
58
+ continue
59
+ if not diff_started:
60
+ continue
61
+
62
+ if line.startswith('---'):
63
+ hunk_lines = [line] # new file
64
+ leftfn = line[4:]
65
+ elif line.startswith('+++'):
66
+ hunk_lines.append(line)
67
+ rightfn = line[4:]
68
+ elif line.startswith('@@'):
69
+ hunk_lines.append(line)
70
+ _, linenums, context = line.split('@@')
71
+ leftlinenums, rightlinenums = linenums.split()
72
+ leftstart, leftcount = _parseStartCount(leftlinenums[1:])
73
+ rightstart, rightcount = _parseStartCount(rightlinenums[1:])
74
+ current_hunk = AttrDict(
75
+ a_fn=leftfn,
76
+ b_fn=rightfn,
77
+ context=context,
78
+ a_lineno=int(leftstart),
79
+ a_count=0,
80
+ b_lineno=int(rightstart),
81
+ b_count=0,
82
+ lines=hunk_lines
83
+ )
84
+ yield current_hunk
85
+ hunk_lines = hunk_lines[:2] # keep file context only
86
+
87
+ elif line[0] in ' +-':
88
+ current_hunk.lines.append(line)
89
+ if line[0] == '+':
90
+ current_hunk.a_count += 1
91
+ elif line[0] == '-':
92
+ current_hunk.b_count += 1
93
+
94
+ def openRow(self, row):
95
+ return HunkViewer(f'{row.a_fn}:{row.a_lineno}', source=self.source, hunks=[row])
96
+
97
+
98
+ class HunkViewer(GitSheet):
99
+ colorizers = [
100
+ RowColorizer(4, 'color_git_hunk_add', lambda s,c,r,v: r and r.old != r.new and r.old is None),
101
+ RowColorizer(4, 'color_git_hunk_del', lambda s,c,r,v: r and r.old != r.new and r.new is None),
102
+ RowColorizer(5, 'color_git_hunk_diff', lambda s,c,r,v: r and r.old != r.new and r.new is not None and r.old is not None),
103
+ ]
104
+ columns = [
105
+ ItemColumn('1', 'old', width=40),
106
+ ItemColumn('2', 'new', width=40),
107
+ ]
108
+
109
+ def draw(self, scr):
110
+ self.column('1').width=self.windowWidth//2-1
111
+ self.column('2').width=self.windowWidth//2-1
112
+ super().draw(scr)
113
+
114
+ def iterload(self):
115
+ nextDelIdx = None
116
+ for hunk in self.hunks:
117
+ for line in hunk.lines[3:]: # diff without the patch headers
118
+ typech = line[0]
119
+ line = line[1:]
120
+ if typech == '-': # deleted
121
+ yield AttrDict(hunk=hunk, type=typech, old=line)
122
+ if nextDelIdx is None:
123
+ nextDelIdx = len(self.rows)-1
124
+ elif typech == '+': # added
125
+ if nextDelIdx is not None:
126
+ if nextDelIdx < len(self.rows):
127
+ self.rows[nextDelIdx].new = line
128
+ nextDelIdx += 1
129
+ continue
130
+
131
+ yield AttrDict(hunk=hunk, type=typech, new=line)
132
+ nextDelIdx = None
133
+ elif typech == ' ': # unchanged
134
+ yield AttrDict(hunk=hunk, type=typech, old=line, new=line)
135
+ nextDelIdx = None
136
+ else:
137
+ continue # header
138
+
139
+
140
+ HunkViewer.addCommand('2', 'git-apply-hunk', 'source.git_apply(cursorRow.hunk, "--cached"); reload()', 'apply this hunk to the index and move to the next hunk')
141
+ HunkViewer.addCommand('1', 'git-remove-hunk', 'source.git_apply(cursorRow.hunk, "--reverse"); reload()', 'remove this hunk from staging')
142
+ HunkViewer.addCommand('Enter', 'git-skip-hunk', 'hunks.pop(0); reload()', 'move to the next hunk without applying this hunk')
143
+ HunkViewer.addCommand('d', 'delete-line', 'source[7].pop(cursorRow[3]); reload()', 'delete a line from the patch')
144
+
145
+ #HunksSheet.addCommand('g^J', 'git-diff-selected', 'vd.push(HunkViewer(selectedRows or rows, source=sheet))', 'view the diffs for the selected hunks (or all hunks)')
146
+
147
+ @GitDiffSheet.api
148
+ def git_apply(sheet, row, *args):
149
+ sheet.git('apply -p0 -', *args, _in='\n'.join(row.lines)+'\n')
150
+
151
+ c = sheet.hunkCount(row)
152
+ vd.status(f'applied hunk ({c})')
153
+ sheet.reload()
154
+
155
+
156
+ #DiffSheet.addCommand('[', '', 'cursorRowIndex = findDiffRow(cursorCol.refnum, cursorRowIndex, -1)', 'go to previous diff')
157
+ #DiffSheet.addCommand(']', '', 'cursorRowIndex = findDiffRow(cursorCol.refnum, cursorRowIndex, +1)', 'go to next diff')
158
+
159
+ GitDiffSheet.addCommand('a', 'git-add-hunk', 'git_apply(cursorRow, "--cached")', 'apply this hunk to the index')
160
+
161
+ vd.addMenuItems('''
162
+ Git > Stage > current hunk > git-add-hunk
163
+ Git > Stage > current hunk > git-add-hunk
164
+ ''')
165
+
166
+ vd.addGlobals(
167
+ GitDiffSheet=GitDiffSheet,
168
+ HunkViewer=HunkViewer
169
+ )
@@ -0,0 +1,161 @@
1
+ import io
2
+
3
+ from visidata import AttrDict, vd, Path, asyncthread, Sheet
4
+
5
+
6
+ class GitSheet(Sheet):
7
+ @property
8
+ def gitargstr(self):
9
+ return ' '.join(self.gitargs)
10
+
11
+ def git(self, subcmd, *args, **kwargs):
12
+ 'For non-modifying commands; not logged except in debug mode'
13
+ sh = vd.importExternal('sh')
14
+ args = list(subcmd.split()) + list(args)
15
+ vd.debug('git ' + ' '.join(str(x) for x in args))
16
+ return sh.git(*args,
17
+ _cwd=self.gitRootPath,
18
+ **kwargs)
19
+
20
+ def loggit(self, subcmd, *args, **kwargs):
21
+ 'Run git command with *args*, and post a status message.'
22
+ import sh
23
+ args = list(subcmd.split()) + list(args)
24
+ vd.warning('git ' + ' '.join(str(x) for x in args))
25
+ return sh.git(*args,
26
+ _cwd=self.gitRootPath,
27
+ **kwargs)
28
+
29
+ def git_all(self, *args, **kwargs):
30
+ 'Return entire output of git command.'
31
+ sh = vd.importExternal('sh')
32
+ try:
33
+ vd.debug('git ' + ' '.join(str(x) for x in args))
34
+ out = self.git('--no-pager',
35
+ *args,
36
+ _decode_errors='replace',
37
+ _bg_exc=False,
38
+ **kwargs)
39
+ except sh.ErrorReturnCode as e:
40
+ vd.warning('git '+' '.join(str(x) for x in args), 'error=%s' % e.exit_code)
41
+ out = e.stdout
42
+
43
+ return out
44
+
45
+ def git_lines(self, subcmd, *args, **kwargs):
46
+ 'Generator of stdout lines from given git command'
47
+ sh = vd.importExternal('sh')
48
+ err = io.StringIO()
49
+ args = list(subcmd.split()) + list(args)
50
+ try:
51
+ vd.debug('git ' + ' '.join(str(x) for x in args))
52
+ for line in self.git('--no-pager',
53
+ *args,
54
+ _decode_errors='replace',
55
+ _iter=True,
56
+ _bg_exc=False,
57
+ _err=err,
58
+ **kwargs):
59
+ yield line[:-1] # remove EOL
60
+
61
+ except sh.ErrorReturnCode as e:
62
+ vd.warning('git '+' '.join(str(x) for x in args), 'error=%s' % e.exit_code)
63
+
64
+ errlines = err.getvalue().splitlines()
65
+ if errlines:
66
+ vd.warning('git stderr: ' + '\n'.join(errlines))
67
+
68
+
69
+ def git_iter(self, subcmd, *args, sep='\0', **kwargs):
70
+ 'Generator of chunks of stdout from given git command *subcmd*, delineated by sep character.'
71
+ sh = vd.importExternal('sh')
72
+ import sh
73
+ err = io.StringIO()
74
+
75
+ args = list(subcmd.split()) + list(args)
76
+ bufsize = 512
77
+ chunks = []
78
+ try:
79
+ vd.debug('git ' + ' '.join(str(x) for x in args))
80
+ for data in self.git('--no-pager',
81
+ *args,
82
+ _decode_errors='replace',
83
+ _out_bufsize=bufsize,
84
+ _iter=True,
85
+ _bg_exc=False,
86
+ _err=err,
87
+ **kwargs):
88
+ while True:
89
+ i = data.find(sep)
90
+ if i < 0:
91
+ break
92
+ chunks.append(data[:i])
93
+ data = data[i+1:]
94
+ yield ''.join(chunks)
95
+ chunks.clear()
96
+
97
+ chunks.append(data)
98
+ except sh.ErrorReturnCode as e:
99
+ vd.warning('git '+' '.join(str(x) for x in args), 'error=%s' % e.exit_code)
100
+
101
+ if chunks:
102
+ yield ''.join(chunks)
103
+
104
+ errlines = err.getvalue().splitlines()
105
+ if errlines:
106
+ vd.warning('git stderr: ' + '\n'.join(errlines))
107
+
108
+ @asyncthread
109
+ def modifyGit(self, *args, **kwargs):
110
+ 'Run git command that modifies the repo'
111
+ vd.warning('git ' + ' '.join(str(x) for x in args))
112
+ ret = self.git_all(*args, **kwargs)
113
+ vd.status(ret)
114
+
115
+ if isinstance(self.source, GitSheet):
116
+ self.source.reload()
117
+
118
+ self.reload()
119
+
120
+ @property
121
+ def gitRootSheet(self):
122
+ if isinstance(self.source, GitSheet):
123
+ return self.source.gitRootSheet
124
+ return self
125
+
126
+ def iterload(self):
127
+ for line in self.git_lines(*self.gitargs):
128
+ yield AttrDict(line=line)
129
+
130
+
131
+ @GitSheet.lazy_property
132
+ def gitRootPath(self):
133
+ 'Return Path of git root (nearest ancestor directory with a .git/)'
134
+ def _getRepoPath(p):
135
+ 'Return path at p or above which has .git subdir'
136
+ if p.joinpath('.git').exists():
137
+ return p
138
+ if str(p) in ['/','']:
139
+ return None
140
+ return _getRepoPath(p.resolve().parent)
141
+
142
+ p = _getRepoPath(self.gitRootSheet.source)
143
+ if p:
144
+ return p
145
+
146
+
147
+ @GitSheet.lazy_property
148
+ def branch(self):
149
+ return self.git('rev-parse', '--abbrev-ref', 'HEAD').strip()
150
+
151
+
152
+ GitSheet.options.disp_note_none = ''
153
+ GitSheet.options.disp_status_fmt = '{sheet.progressStatus}‹{sheet.branchStatus}› {sheet.name}| '
154
+
155
+ GitSheet.addCommand('gi', 'git-exec', 'cmdstr=input("gi", type="git"); vd.push(GitSheet(cmdstr, gitargs=cmdstr.split()))', 'execute git command')
156
+
157
+ GitSheet.addCommand('Alt+g', 'menu-git', 'pressMenu("Git")', '')
158
+
159
+ vd.addMenuItems('''
160
+ Git > Execute command > git-exec
161
+ ''')
@@ -0,0 +1,37 @@
1
+ from visidata import vd, VisiData, Path, ColumnItem, ESC
2
+
3
+ from .gitsheet import GitSheet
4
+
5
+
6
+ @VisiData.api
7
+ def git_grep(vd, p, args):
8
+ return GitGrep(args[0], regex=args[0], source=p)
9
+
10
+
11
+ class GitGrep(GitSheet):
12
+ rowtype = 'results' # rowdef: list(file, line, line_contents)
13
+ guide = '''
14
+ # vgit grep
15
+ Each row on this sheet is a line matching the regex pattern `{sheet.regex}` in the tracked files of the current directory.
16
+
17
+ - `Ctrl+O` to open _{sheet.cursorRow[0]}:{sheet.cursorRow[1]}_ in the system editor; saved changes will be reflected automatically.
18
+ '''
19
+ columns = [
20
+ ColumnItem('file', 0, help='filename of the match'),
21
+ ColumnItem('line', 1, help='line number within file'),
22
+ ColumnItem('text', 2, width=120, help='matching line of text'),
23
+ ]
24
+ nKeys = 2
25
+
26
+ def iterload(self):
27
+ tmp = (self.topRowIndex, self.cursorRowIndex)
28
+ for line in self.git_lines('grep', '--no-color', '-z', '--line-number', '--ignore-case', self.regex):
29
+ # line = line.replace(ESC+'[1;31m', '[:green]')
30
+ # line = line.replace(ESC+'[m', '[/]')
31
+ yield list(line.split('\0'))
32
+ self.topRowIndex, self.cursorRowIndex = tmp
33
+
34
+
35
+ GitSheet.addCommand('g/', 'git-grep', 'rex=inputRegex("git grep: "); vd.push(GitGrep(rex, regex=rex, source=sheet))', 'find in all files in this repo')
36
+ GitGrep.addCommand('Ctrl+O', 'sysopen-row', 'launchExternalEditorPath(Path(cursorRow[0]), linenum=cursorRow[1]); reload()', 'open this file in $EDITOR')
37
+ GitGrep.bindkey('Enter', 'sysopen-row')
@@ -0,0 +1,81 @@
1
+ import functools
2
+
3
+ from visidata import vd, VisiData, Column, ItemColumn, date, RowColorizer, asyncthread, Progress, AttrDict
4
+
5
+ from .gitsheet import GitSheet
6
+
7
+
8
+ @VisiData.api
9
+ def git_log(vd, p, *args):
10
+ return GitLogSheet('git-log', source=p, gitargs=args)
11
+
12
+ # rowdef: (commit_hash, refnames, author, author_date, body, notes)
13
+ class GitLogSheet(GitSheet):
14
+ guide = '''
15
+ # git log {sheet.gitargstr}
16
+ {sheet.cursorRow.message}
17
+ '''
18
+ GIT_LOG_FORMAT = ['%H', '%D', '%an <%ae>', '%ai', '%B', '%N']
19
+ rowtype = 'commits' # rowdef: AttrDict
20
+ defer = True
21
+ savesToSource = True
22
+ columns = [
23
+ ItemColumn('commitid', width=8),
24
+ ItemColumn('refnames', width=12),
25
+ ItemColumn('message', type=str.strip, setter=lambda c,r,v: c.sheet.git('commit --amend --no-edit --quiet --message', v), width=50),
26
+ ItemColumn('author', setter=lambda c,r,v: c.sheet.git('commit --amend --no-edit --quiet --author', v)),
27
+ ItemColumn('author_date', type=date, setter=lambda c,r,v: c.sheet.git('commit --amend --no-edit --quiet --date', v)),
28
+ ItemColumn('notes', setter=lambda c,r,v: c.sheet.git('notes add --force --message', v, r.commitid)),
29
+ ]
30
+ colorizers = [
31
+ RowColorizer(5, 'color_vgit_unpushed', lambda s,c,r,v: r and not s.inRemoteBranch(r.commitid)),
32
+ ]
33
+
34
+ def __init__(self, *args, **kwargs):
35
+ super().__init__(*args, **kwargs)
36
+
37
+ @functools.lru_cache()
38
+ def inRemoteBranch(self, commitid):
39
+ return self.git_all('branch -r --contains', commitid, _ok_code=[0, 1])
40
+
41
+ def iterload(self):
42
+ lines = self.git_iter('log --no-color -z', '--pretty=format:' + '%x1f'.join(self.GIT_LOG_FORMAT), *self.gitargs)
43
+ for record in Progress(tuple(lines)):
44
+ r = record.split('\x1f')
45
+ yield AttrDict(
46
+ commitid=r[0],
47
+ refnames=r[1],
48
+ author=r[2],
49
+ author_date=r[3],
50
+ message=r[4],
51
+ notes=r[5],
52
+ )
53
+
54
+ def openRow(self, row):
55
+ 'open this commit'
56
+ return getCommitSheet(row[0][:7], self, row[0])
57
+
58
+ @asyncthread
59
+ def commit(self, path, adds, mods, dels):
60
+
61
+ assert not adds
62
+ assert not dels
63
+
64
+ for row, rowmods in mods.values():
65
+ for col, val in rowmods.values():
66
+ vd.callNoExceptions(col.putValue, row, val)
67
+
68
+ self.reload()
69
+ self.resetDeferredCommit()
70
+
71
+
72
+ GitLogSheet.addCommand(None, 'delete-row', 'error("delete is not supported")')
73
+ GitLogSheet.addCommand(None, 'add-row', 'error("commits cannot be added")')
74
+ #GitLogSheet.addCommand('x', 'git-pick', 'git("cherry-pick", cursorRow.commitid)', 'cherry-pick this commit onto current branch')
75
+ #GitLogSheet.addCommand('r', 'git-reset-here', 'git("update-ref", "refs/heads/"+source, cursorRow[0])', 'reset this branch to this commit')
76
+
77
+ GitSheet.addCommand('', 'git-log', 'vd.push(git_log(gitRootPath, branch))', 'push log of current branch')
78
+
79
+ vd.addMenuItems('''
80
+ Git > Open > log > git-log
81
+ ''')
@@ -0,0 +1,55 @@
1
+ '''
2
+ # vgit: VisiData wrapper for git
3
+
4
+ The syntax for vgit is the same as the syntax for git.
5
+ By default, will pass the command to git verbatim, as quickly as possible.
6
+ If vgit can provide an interactive interface for a particular subcommand,
7
+ it will open the sheet returned by vd.git_<subcommand>(path, args).
8
+ '''
9
+
10
+ import os
11
+ import sys
12
+
13
+
14
+ def vgit_cli():
15
+ import visidata
16
+ from visidata import vd, Path
17
+
18
+ args = sys.argv[1:]
19
+ flDebug = '--debug' in args
20
+ if flDebug:
21
+ args.remove('--debug')
22
+
23
+ if not args:
24
+ args = ['help']
25
+
26
+ func = getattr(vd, 'git_'+args[0], None)
27
+ if func:
28
+ vd.loadConfigAndPlugins()
29
+ vd.status(visidata.__version_info__)
30
+ vd.domotd()
31
+ if flDebug:
32
+ vd.options.debug = True
33
+
34
+ rc = 0
35
+ try:
36
+ p = Path('.')
37
+ vs = func(p, args[1:])
38
+ if vs:
39
+ vd.run(vs)
40
+ except BrokenPipeError:
41
+ os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno()) # handle broken pipe gracefully
42
+ except visidata.ExpectedException as e:
43
+ print(str(e))
44
+ except Exception as e:
45
+ rc = 1
46
+ vd.exceptionCaught(e)
47
+ if flDebug:
48
+ raise
49
+
50
+ sys.stderr.flush()
51
+ sys.stdout.flush()
52
+ os._exit(rc) # cleanup can be expensive
53
+
54
+ import subprocess
55
+ return subprocess.run(['git', *args]).returncode
@@ -0,0 +1,57 @@
1
+ from visidata import vd, VisiData, ItemColumn, AttrDict, RowColorizer, Path
2
+
3
+ from .gitsheet import GitSheet
4
+
5
+ @VisiData.api
6
+ def git_remote(vd, p, args):
7
+ if not args or 'show' in args:
8
+ return GitRemotes('remotes', source=p)
9
+
10
+
11
+ class GitRemotes(GitSheet):
12
+ guide = '''
13
+ # git remote
14
+ Manage the set of repositories ("remotes") whose branches you track.
15
+
16
+ - `a` to add a remote
17
+ - `d` to mark a remote for deletion
18
+ - `e` to edit the _remote_ or _url_
19
+ - `z Ctrl+S` to commit the changes.
20
+ '''
21
+ rowtypes = 'remotes' # rowdef: dict(remote=, url=, type=)
22
+ columns=[
23
+ ItemColumn('remote', setter=lambda c,r,v: c.sheet.set_remote(c,r,v)),
24
+ ItemColumn('type'),
25
+ ItemColumn('url', width=40, setter=lambda c,r,v: c.sheet.set_url(c,r,v)),
26
+ ]
27
+ nKeys = 1
28
+ defer = True
29
+
30
+ def set_remote(self, col, row, val):
31
+ self.loggit('remote', 'rename', self.column('remote').getSourceValue(row), val)
32
+
33
+ def set_url(self, col, row, val):
34
+ self.loggit('remote', 'set-url', row.remote, val)
35
+
36
+ def iterload(self):
37
+ for line in self.git_lines('remote', '-v', 'show'):
38
+ name, url, paren_type = line.split()
39
+ yield AttrDict(remote=name, url=url, type=paren_type[1:-1])
40
+
41
+ def commitDeleteRow(self, row):
42
+ self.loggit('remote', 'remove', row.remote)
43
+
44
+ def commitAddRow(self, row):
45
+ row.remote = self.column('remote').getValue(row)
46
+ row.url = self.column('url').getValue(row)
47
+ self.loggit('remote', 'add', row.remote, row.url)
48
+
49
+ def newRow(self):
50
+ return AttrDict()
51
+
52
+
53
+ GitSheet.addCommand('', 'git-open-remotes', 'vd.push(git_remote(Path("."), ""))', 'open git remotes sheet')
54
+
55
+ vd.addMenuItems('''
56
+ Git > Open > remotes > git-open-remotes
57
+ ''')
@@ -0,0 +1,71 @@
1
+ from visidata import vd, VisiData, Sheet, Column, AttrColumn, date, vlen, asyncthread, Path, namedlist, PyobjSheet, modtime, AttrDict
2
+
3
+ from .gitsheet import GitSheet
4
+
5
+ @VisiData.api
6
+ def guess_git(vd, p):
7
+ if (p/'.git').is_dir():
8
+ return dict(filetype='git', _likelihood=10)
9
+
10
+
11
+ @VisiData.api
12
+ def open_git(vd, p):
13
+ return vd.git_status(p, [])
14
+
15
+
16
+ @VisiData.api
17
+ def git_repos(vd, p, args):
18
+ return GitRepos(p.base_stem, source=p)
19
+
20
+
21
+ class GitLinesColumn(Column):
22
+ def __init__(self, name, cmd, *args, **kwargs):
23
+ super().__init__(name, cache='async', **kwargs)
24
+ cmdparts = cmd.split()
25
+ if cmdparts[0] == 'git':
26
+ cmdparts = cmdparts[1:]
27
+ self.gitargs = cmdparts + list(args)
28
+
29
+ def calcValue(self, r):
30
+ lines = list(GitSheet(source=r).git_lines(*self.gitargs))
31
+ if lines:
32
+ return lines
33
+
34
+
35
+ class GitAllColumn(GitLinesColumn):
36
+ def calcValue(self, r):
37
+ return GitSheet(source=r).git_all(*self.gitargs).strip()
38
+
39
+
40
+
41
+ class GitRepos(GitSheet):
42
+ guide = '''
43
+ # git repos
44
+ A list of git repositories under `{sheet.source}`
45
+
46
+ - `Enter` to open the status sheet for the current repo
47
+ '''
48
+ rowtype = 'git repos' # rowdef: Path
49
+ columns = [
50
+ Column('repo', type=str, width=30),
51
+ GitAllColumn('branch', 'git rev-parse --abbrev-ref HEAD', width=8),
52
+ GitLinesColumn('diffs', 'git diff --no-color', type=vlen, width=8),
53
+ GitLinesColumn('staged_diffs', 'git diff --cached', type=vlen, width=8),
54
+ GitLinesColumn('branches', 'git branch --no-color', type=vlen, width=10),
55
+ GitLinesColumn('stashes', 'git stash list', type=vlen, width=8),
56
+ Column('modtime', type=date, getter=lambda c,r: modtime(r)),
57
+ ]
58
+ nKeys = 1
59
+
60
+ def iterload(self):
61
+ import glob
62
+ for fn in glob.glob('**/.git', root_dir=self.source, recursive=True):
63
+ yield Path(fn).parent
64
+
65
+
66
+ def openRow(self, row):
67
+ return vd.git_status(row, [])
68
+
69
+ def openCell(self, col, row):
70
+ val = col.getValue(row)
71
+ return PyobjSheet(getattr(val, '__name__', ''), source=val)
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from setuptools import setup, find_packages
4
+
5
+ # Note: use `python3 visidata/apps/setup.py install` from the root directory
6
+
7
+ __version__ = '0.2-dev'
8
+
9
+ setup(name='vgit',
10
+ version=__version__,
11
+ description='a sleek terminal user interface for git',
12
+ # long_description=open('README.md').read(),
13
+ install_requires=['sh<2'], # visidata
14
+ packages=find_packages(exclude=["tests"]),
15
+ scripts=['vgit'],
16
+ entry_points={'visidata.plugins': 'vgit=visidata.apps.vgit'},
17
+ author='Saul Pwanson',
18
+ author_email='vgit@saul.pw',
19
+ url='https://github.com/saulpw/visidata/vgit',
20
+ license='GPLv3',
21
+ python_requires='>=3.7',
22
+ classifiers=[
23
+ 'Development Status :: 2 - Pre-Alpha',
24
+ 'Environment :: Console',
25
+ 'Environment :: Console :: Curses',
26
+ 'Intended Audience :: Developers',
27
+ 'Intended Audience :: System Administrators',
28
+ 'Intended Audience :: Information Technology',
29
+ 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
30
+ 'Operating System :: OS Independent',
31
+ 'Programming Language :: Python :: 3',
32
+ 'Topic :: Utilities',
33
+ 'Topic :: Software Development :: Version Control',
34
+ 'Topic :: Terminals'
35
+ ],
36
+ keywords=('console textpunk git version-control curses visidata tui terminal'),
37
+ )