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,61 @@
1
+ '''
2
+ Specify the billing_project_id as the netloc, and the actual dataset_id as the path:
3
+
4
+ vdsql bigquery://<billing_project>/<dataset_id>''
5
+ '''
6
+
7
+
8
+ from visidata import vd, VisiData, Sheet, AttrColumn
9
+ from . import IbisTableSheet, IbisTableIndexSheet, IbisConnectionPool
10
+
11
+ import ibis
12
+ import ibis.expr.operations as ops
13
+
14
+ @VisiData.api
15
+ def openurl_bigquery(vd, p, filetype=None):
16
+ vd.configure_ibis()
17
+ vd.configure_bigquery()
18
+ return BigqueryDatabaseIndexSheet(p.base_stem, source=p, ibis_con=None)
19
+
20
+ vd.openurl_bq = vd.openurl_bigquery
21
+
22
+
23
+ @VisiData.api
24
+ def configure_bigquery(vd):
25
+ @ibis.bigquery.add_operation(ops.TimestampDiff)
26
+ def bq_timestamp_diff(t, expr):
27
+ op = expr.op()
28
+ left = t.translate(op.left)
29
+ right = t.translate(op.right)
30
+ return f"TIMESTAMP_DIFF({left}, {right}, SECOND)"
31
+
32
+
33
+ class BigqueryDatabaseIndexSheet(Sheet):
34
+ rowtype = 'databases' # rowdef: DatasetListItem
35
+ columns = [
36
+ # AttrColumn('project', width=0),
37
+ AttrColumn('dataset_id'),
38
+ AttrColumn('friendly_name'),
39
+ AttrColumn('full_dataset_id', width=0),
40
+ AttrColumn('labels'),
41
+ ]
42
+ nKeys = 1
43
+
44
+ @property
45
+ def con(self):
46
+ if not self.ibis_con:
47
+ import ibis
48
+ self.ibis_con = ibis.connect(self.source)
49
+ return self.ibis_con
50
+
51
+ def iterload(self):
52
+ yield from self.con.client.list_datasets(project=self.source.name)
53
+
54
+ def openRow(self, row):
55
+ return IbisTableIndexSheet(row.dataset_id,
56
+ database_name=self.source.name+'.'+row.dataset_id,
57
+ ibis_con=self.con,
58
+ ibis_conpool=IbisConnectionPool(f"{self.source}/{row.dataset_id}"),
59
+ source=row,
60
+ filetype=None,
61
+ sheet_type=IbisTableSheet)
@@ -0,0 +1,53 @@
1
+ import time
2
+
3
+ from visidata import BaseException, vd, VisiData, Progress
4
+
5
+ from ._ibis import IbisTableSheet, IbisTableIndexSheet, IbisConnectionPool
6
+
7
+
8
+ @VisiData.api
9
+ def openurl_clickhouse(vd, p, filetype=None):
10
+ vd.configure_ibis()
11
+ return IbisTableIndexSheet(p.base_stem, source=p, filetype=None, database_name=None,
12
+ ibis_conpool=IbisConnectionPool(p), sheet_type=ClickhouseSheet)
13
+
14
+ vd.openurl_clickhouses = vd.openurl_clickhouse
15
+
16
+
17
+ class ClickhouseSheet(IbisTableSheet):
18
+ @property
19
+ def countRows(self):
20
+ if self.total_rows is not None:
21
+ return self.total_rows
22
+ return super().countRows
23
+
24
+ def iterload(self):
25
+ with self.con as con:
26
+ qid = None
27
+ try:
28
+ if self.query is None:
29
+ self.query = self.baseQuery(con)
30
+
31
+ self.reloadColumns(self.query, start=0) # columns based on query without metadata
32
+ sqlstr = con.compile(self.query.limit(self.options.ibis_limit or None))
33
+
34
+ with Progress(gerund='clickhousing', sheet=self) as prog:
35
+ settings = {'max_block_size': 10000}
36
+ with con.con.query_rows_stream(sqlstr, settings) as stream:
37
+ prog.total = int(stream.source.summary['total_rows_to_read'])
38
+ prog.made = 0
39
+ for row in stream:
40
+ prog.made += 1
41
+ yield row
42
+ self.total_rows = prog.total
43
+
44
+ except Exception as e:
45
+ raise
46
+ except BaseException:
47
+ if qid:
48
+ con.con.cancel(qid)
49
+
50
+
51
+ ClickhouseSheet.init('total_rows', lambda: None)
52
+
53
+ #ClickhouseSheet.class_options.sql_always_count = True
@@ -0,0 +1,40 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ from setuptools import setup, find_packages
4
+ from pathlib import Path
5
+
6
+
7
+ exec(Path('__about__.py').read_text())
8
+
9
+
10
+ def readme():
11
+ return Path("README.md").read_text()
12
+
13
+
14
+ def requirements():
15
+ return Path("requirements.txt").read_text().splitlines()
16
+
17
+ def requirements_extra():
18
+ return Path("requirements-extra.txt").read_text().splitlines()
19
+
20
+
21
+ setup(
22
+ name="vdsql",
23
+ version=__version__,
24
+ description=__description__,
25
+ long_description=readme(),
26
+ long_description_content_type="text/markdown",
27
+ classifiers=[
28
+ "Development Status :: 4 - Beta",
29
+ "Programming Language :: Python :: 3",
30
+ ],
31
+ keywords="visidata sql rdbms ibis substrait",
32
+ author="Saul Pwanson",
33
+ url="https://github.com/visidata/vdsql",
34
+ python_requires=">=3.9",
35
+ packages=find_packages(exclude=["tests"]),
36
+ entry_points={'visidata.plugins': 'vdsql=visidata.apps.vdsql'},
37
+ scripts=['vdsql'],
38
+ install_requires=requirements(),
39
+ extra_requires=requirements_extra(),
40
+ )
@@ -0,0 +1,67 @@
1
+ import time
2
+
3
+ from visidata import vd, BaseException, VisiData
4
+
5
+ from ._ibis import IbisTableSheet, IbisConnectionPool, IbisTableIndexSheet
6
+
7
+
8
+ @VisiData.api
9
+ def openurl_snowflake(vd, p, filetype=None):
10
+ return IbisTableIndexSheet(p.base_stem, source=p, filetype=None, database_name=None,
11
+ ibis_conpool=IbisConnectionPool(p),
12
+ sheet_type=SnowflakeSheet)
13
+
14
+
15
+ class SnowflakeSheet(IbisTableSheet):
16
+ @property
17
+ def countRows(self):
18
+ r = super().countRows
19
+ if r is None and self.cursor is None:
20
+ return None # no cursor yet
21
+ return r
22
+
23
+ def executeSql(self, sql):
24
+ assert self.cursor is None
25
+
26
+ with self.con as con:
27
+ con = con.con
28
+
29
+ if self.warehouse:
30
+ con.execute(f'USE WAREHOUSE {self.warehouse}')
31
+
32
+ with con.begin() as c:
33
+ snowflake_conn = c.connection.dbapi_connection
34
+ cursor = self.cursor = snowflake_conn.cursor()
35
+ cursor.execute_async(sql)
36
+ while snowflake_conn.is_still_running(snowflake_conn.get_query_status(cursor.sfqid)):
37
+ time.sleep(.1)
38
+
39
+ cursor.get_results_from_sfqid(cursor.sfqid)
40
+ yield from cursor.fetchall()
41
+
42
+ self.cursor = None
43
+
44
+ def iterload(self):
45
+ try:
46
+ with self.con as con:
47
+ if self.query is None:
48
+ self.query = self.baseQuery(con)
49
+ yield from self.executeSql(self.ibis_to_sql(self.withRowcount(self.baseQuery(con))))
50
+ except BaseException:
51
+ if self.cursor:
52
+ self.cancelQuery(self.cursor.sfqid)
53
+ raise
54
+
55
+ self.reloadColumns(self.query) # columns based on query without metadata
56
+
57
+ def cancelQuery(self, qid):
58
+ vd.status(f'canceling "{qid}"')
59
+ with self.con as con:
60
+ with con.begin() as con:
61
+ cursor = con.connection.dbapi_connection.cursor()
62
+ cursor.execute(f"SELECT SYSTEM$CANCEL_QUERY('{qid}')")
63
+ vd.status(cursor.fetchall())
64
+
65
+
66
+ SnowflakeSheet.init('cursor', lambda: None)
67
+ SnowflakeSheet.init('warehouse', str)
@@ -0,0 +1,13 @@
1
+ from . import abort, statusbar
2
+ from . import (
3
+ grep,
4
+ config,
5
+ branch,
6
+ remote,
7
+ blame,
8
+ status,
9
+ log,
10
+ diff,
11
+ stash,
12
+ repos,
13
+ )
@@ -0,0 +1,3 @@
1
+ from .main import vgit_cli
2
+
3
+ vgit_cli()
@@ -0,0 +1,23 @@
1
+ from visidata import vd, Menu
2
+ from .gitsheet import GitSheet
3
+
4
+
5
+ @GitSheet.api
6
+ def abortWhatever(sheet):
7
+ inp = sheet.gitInProgress()
8
+ if inp.startswith('cherry-pick'):
9
+ sheet.modifyGit('cherry-pick', '--abort')
10
+ elif inp.startswith('merg'):
11
+ sheet.modifyGit('merge', '--abort')
12
+ elif inp.startswith('bisect'):
13
+ sheet.modifyGit('bisect', 'reset')
14
+ elif inp.startswith('rebas') or inp.startswith('apply'):
15
+ sheet.modifyGit('rebase', '--abort') # or --quit?
16
+ else:
17
+ vd.status('nothing to abort')
18
+
19
+
20
+ GitSheet.addCommand('^A', 'git-abort', 'abortWhatever()', 'abort the current in-progress action')
21
+
22
+
23
+ vd.addMenuItems('Git > Abort > git-abort')
@@ -0,0 +1,76 @@
1
+ from visidata import vd, Column, VisiData, ItemColumn, Path, AttrDict, date
2
+
3
+ from .gitsheet import GitSheet
4
+
5
+
6
+ @VisiData.api
7
+ def git_blame(vd, gitpath, args, **kwargs):
8
+ if args and not args[-1].startswith('-'):
9
+ fn = args[-1]
10
+ return GitBlame('blame', fn, source=Path(fn), **kwargs)
11
+
12
+
13
+ class FormatColumn(Column):
14
+ def calcValue(self, row):
15
+ return self.expr.format(**row)
16
+
17
+
18
+ # rowdef: (hdr, orig_linenum, linenum, line)
19
+ # hdr = { 'sha': .., 'orig_linenum': .., 'final_linenum': .. }
20
+ # source = GitSheet; .gitfile=GitFile
21
+ class GitBlame(GitSheet):
22
+ rowtype = 'lines'
23
+ guide = '''
24
+ # git blame
25
+ '''
26
+ columns = [
27
+ ItemColumn('sha', width=0),
28
+ ItemColumn('orig_linenum', width=0, type=int),
29
+ ItemColumn('final_linenum', width=0, type=int),
30
+ ItemColumn('author', width=15),
31
+ ItemColumn('author_time', width=13, type=date),
32
+ FormatColumn('committer', width=0, expr='{committer} {committer_mail}'),
33
+ ItemColumn('committer_time', width=0, type=date),
34
+ ItemColumn('linenum', width=6, type=int),
35
+ ItemColumn('line', width=72),
36
+ ]
37
+
38
+ def iterload(self):
39
+ lines = list(self.git_lines('blame', '--porcelain', str(self.source)))
40
+ i = 0
41
+ headers = {} # [sha1] -> hdr
42
+ while i < len(lines):
43
+ # header
44
+ parts = lines[i].split()
45
+ sha, orig, final = parts[:3]
46
+ if len(parts) > 3:
47
+ nlines_this_group = parts[3]
48
+
49
+ if sha not in headers:
50
+ hdr = AttrDict(sha=sha, orig_linenum=orig, final_linenum=final)
51
+ headers[sha] = hdr
52
+ else:
53
+ hdr = headers[sha]
54
+
55
+ while lines[i][0] != '\t':
56
+ try:
57
+ k, v = lines[i].split(maxsplit=1)
58
+ k = k.replace('-', '_')
59
+ if '_time' in k:
60
+ v = int(v)
61
+ hdr[k] = v
62
+ except Exception:
63
+ vd.status(lines[i])
64
+ i += 1
65
+
66
+ yield AttrDict(
67
+ linenum=final,
68
+ line=lines[i][1:],
69
+ **hdr
70
+ )
71
+ i += 1
72
+
73
+
74
+ #GitBlame.addCommand(ENTER, 'diff-line', 'openDiff(str(gitfile), cursorRow[0]["sha"]+"^", cursorRow[0]["sha"])', 'open diff of the commit when this line changed')
75
+
76
+ #GitStatus.addCommand(None, 'git-blame', 'vd.push(GitBlame(cursorRow, source=sheet))', 'push blame for this file')
@@ -0,0 +1,153 @@
1
+ import re
2
+
3
+ from visidata import vd, Column, VisiData, ItemColumn, AttrColumn, Path, AttrDict, RowColorizer, date, Progress
4
+
5
+ from .gitsheet import GitSheet
6
+
7
+ vd.theme_option('color_git_current_branch', 'underline', 'color of current branch on branches sheet')
8
+ vd.theme_option('color_git_remote_branch', 'cyan', 'color of remote branches on branches sheet')
9
+
10
+
11
+ @VisiData.api
12
+ def git_branch(vd, p, args):
13
+ nonListArgs = '--track --no-track --set-upstream-to -u --unset-upstream -m -M -c -C -d -D --edit-description'.split()
14
+ if any(x in args for x in nonListArgs):
15
+ return
16
+
17
+ return GitBranch('git-branch-list', source=p, git_args=args)
18
+
19
+
20
+ def _remove_prefix(text, prefix):
21
+ if text.startswith(prefix):
22
+ return text[len(prefix):]
23
+ return text
24
+
25
+
26
+ class GitBranchNameColumn(Column):
27
+ def calcValue(self, row):
28
+ return _remove_prefix(row.localbranch, 'remotes/')
29
+
30
+ def putValue(self, row, val):
31
+ self.sheet.loggit('branch', '-v', '--move', row.localbranch, val)
32
+
33
+
34
+ class GitBranch(GitSheet):
35
+ guide = '''
36
+ # git branch
37
+ List of all branches, including relevant metadata.
38
+
39
+ - `d` to mark a branch for deletion
40
+ - `e` on the _branch_ column to rename the branch
41
+ - `z Ctrl+S` to commit changes
42
+ '''
43
+ defer = True
44
+ rowtype = 'branches' # rowdef: AttrDict from regex (in reload below)
45
+ columns = [
46
+ GitBranchNameColumn('branch', width=20),
47
+ # Column('remote', getter=lambda c,r: r['localbranch'].startswith('remotes/') and '*' or '', width=3),
48
+ ItemColumn('head_commitid', 'refid', width=0),
49
+ ItemColumn('tracking', 'remotebranch'),
50
+ ItemColumn('upstream'),
51
+ ItemColumn('merge_base', 'merge_name', width=20),
52
+ ItemColumn('extra', width=0),
53
+ ItemColumn('head_commitmsg', 'msg', width=50),
54
+ ItemColumn('last_commit', type=date),
55
+ ItemColumn('last_author'),
56
+ ]
57
+ colorizers = [
58
+ RowColorizer(10, 'color_git_current_branch', lambda s,c,r,v: r and r['current']),
59
+ RowColorizer(10, 'color_git_remote_branch', lambda s,c,r,v: r and r['localbranch'].startswith('remotes/')),
60
+ ]
61
+ nKeys = 1
62
+
63
+ def iterload(self):
64
+ branches_lines = self.git_lines(
65
+ 'branch',
66
+ '--list',
67
+ '--format',
68
+ ' '.join((
69
+ '%(if)%(symref)%(then)yes%(else)no%(end)',
70
+ '%(HEAD) %(refname:short) %(objectname:short)',
71
+ '%(if)%(upstream)%(then)[%(upstream:short)',
72
+ '%(if)%(upstream:track)%(then): %(upstream:track,nobracket)%(end)]',
73
+ '%(end)',
74
+ '%(contents:subject)'
75
+ )),
76
+ '-vv',
77
+ '--no-color',
78
+ *self.git_args)
79
+
80
+ for line in branches_lines:
81
+ m = re.match(r'''(?P<is_symref>(yes|no)?)\s+
82
+ (?P<current>\*?)\s+
83
+ (?P<localbranch>\S+)\s+
84
+ (?P<refid>\w+)\s+
85
+ (?:\[
86
+ (?P<remotebranch>[^\s\]:]+):?
87
+ \s*(?P<extra>.*?)
88
+ \])?
89
+ \s*(?P<msg>.*)''', line, re.VERBOSE)
90
+ if not m:
91
+ continue
92
+ branch_details = AttrDict(m.groupdict())
93
+ if branch_details.is_symref == 'yes':
94
+ continue
95
+ yield branch_details
96
+
97
+ branch_stats = self.gitRootSheet.gitBranchStatuses
98
+ for row in Progress(self.rows):
99
+ merge_base = self.git_all("show-branch", "--merge-base", row.localbranch, self.gitRootSheet.branch, _ok_code=[0,1]).strip()
100
+ row.update(dict(
101
+ merge_name = self.git_all("name-rev", "--name-only", merge_base).strip() if merge_base else '',
102
+ upstream = branch_stats.get(row.localbranch),
103
+ last_commit = self.git_all("show", "--no-patch", '--pretty=%ai', row.localbranch).strip(),
104
+ last_author = self.git_all("show", "--no-patch", '--pretty=%an', row.localbranch).strip()
105
+ ))
106
+
107
+ def commitAddRow(self, row):
108
+ self.loggit('branch', row.localbranch)
109
+
110
+ def commitDeleteRow(self, row):
111
+ self.loggit('branch', '--delete', _remove_prefix(row.localbranch, 'remotes/'))
112
+
113
+
114
+ @GitSheet.lazy_property
115
+ def gitBranchStatuses(sheet):
116
+ ret = {} # localbranchname -> "+5/-2"
117
+ for branch_status in sheet.git_lines('for-each-ref', '--format=%(refname:short) %(upstream:short) %(upstream:track)', 'refs/heads'):
118
+ m = re.search(r'''(\S+)\s*
119
+ (\S+)?\s*
120
+ (\[
121
+ (ahead.(\d+)),?\s*
122
+ (behind.(\d+))?
123
+ \])?''', branch_status, re.VERBOSE)
124
+ if not m:
125
+ vd.status('unmatched branch status: ' + branch_status)
126
+ continue
127
+
128
+ localb, remoteb, _, _, nahead, _, nbehind = m.groups()
129
+ if nahead:
130
+ r = '+%s' % nahead
131
+ else:
132
+ r = ''
133
+ if nbehind:
134
+ if r:
135
+ r += '/'
136
+ r += '-%s' % nbehind
137
+ ret[localb] = r
138
+
139
+ return ret
140
+
141
+
142
+ GitSheet.addCommand('', 'git-open-branches', 'vd.push(git_branch(source, []))', 'push branches sheet')
143
+ GitSheet.addCommand('', 'git-branch-create', 'git("branch", input("create branch: ", type="branch"))', 'create a new branch off the current checkout')
144
+ GitBranch.addCommand('', 'git-branch-checkout', 'git("checkout", cursorRow.localbranch)', 'checkout this branch')
145
+
146
+
147
+ vd.addMenuItems('''
148
+ Git > Branch > add > git-branch-create
149
+ Git > Branch > delete > git-branch-delete
150
+ Git > Branch > rename > git-branch-rename
151
+ Git > Branch > checkout > git-branch-checkout
152
+ Git > Open > branches > git-open-branches
153
+ ''')
@@ -0,0 +1,95 @@
1
+ from itertools import islice
2
+
3
+ from visidata import vd, Column, VisiData, ItemColumn, AttrColumn, Path, AttrDict
4
+
5
+ from .gitsheet import GitSheet
6
+
7
+
8
+ CONFIG_CONTEXTS = ('local', 'global', 'system')
9
+
10
+ @VisiData.api
11
+ def git_config(vd, p, args):
12
+ if not args or '-l' in args:
13
+ return GitConfig('git-config', source=p)
14
+
15
+ vd.git_options = vd.git_config
16
+
17
+ def batched(iterable, n=1):
18
+ "Batch data into tuples of length n. The last batch may be shorter."
19
+ # batched('ABCDEFG', 3) --> ABC DEF G
20
+ assert n >= 1, 'n must be at least one'
21
+
22
+ while (batch := tuple(islice(iter(iterable), n))):
23
+ yield batch
24
+
25
+
26
+ class GitConfigColumn(Column):
27
+ def calcValue(self, row):
28
+ return row.get(self.expr)
29
+
30
+ def putValue(self, row, val):
31
+ if val is None:
32
+ self.sheet.loggit('config', '--unset', '--'+self.expr, row['option'])
33
+ else:
34
+ self.sheet.loggit('config', '--'+self.expr, row['option'], val)
35
+
36
+ row[self.expr] = val
37
+
38
+
39
+ class GitConfig(GitSheet):
40
+ guide = '''
41
+ # git config
42
+ Add, edit, or delete git config options.
43
+
44
+ - Make changes using standard commands like `a`dd and `e`dit
45
+ - `z Ctrl+S` to commit changes to git config file.
46
+ '''
47
+ rowtype = 'git options' # rowdef: [scope, origin, opt, val]
48
+ defer = True
49
+ columns = [
50
+ ItemColumn('option', width=20),
51
+ ]
52
+ nKeys = 1
53
+
54
+ def iterload(self):
55
+ cmd = self.git_iter('config', '--list', '--show-scope', '--show-origin', '-z')
56
+ self.gitopts = {}
57
+ scopes = {c.name:c for c in self.columns}
58
+ for row in batched(cmd, 3):
59
+ if len(row) < 3:
60
+ break
61
+
62
+ scope, origin, optval = row
63
+ opt, val = optval.split('\n', 1)
64
+
65
+ if opt in self.gitopts:
66
+ r = self.gitopts[opt]
67
+ else:
68
+ r = AttrDict(option=opt)
69
+ self.gitopts[opt] = r
70
+ yield r
71
+
72
+ r[scope] = val
73
+
74
+ if scope not in scopes:
75
+ c = GitConfigColumn(scope, expr=scope)
76
+ self.addColumn(c)
77
+ scopes[scope] = c
78
+
79
+ def commitDeleteRow(self, row):
80
+ for k in CONFIG_CONTEXTS:
81
+ if row.get(k):
82
+ self.loggit('config', '--unset', '--'+k, row['option'])
83
+
84
+ def commitAddRow(self, row):
85
+ for k in CONFIG_CONTEXTS:
86
+ if row.get(k):
87
+ self.loggit('config', '--add', '--'+k, row['option'], row.get(k))
88
+
89
+
90
+ GitSheet.addCommand('gO', 'git-config', 'vd.push(GitConfig("git_config", source=Path(".")))', 'push sheet of git options')
91
+
92
+
93
+ vd.addMenuItems('''
94
+ Git > Config > git-config
95
+ ''')