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.
- visidata/__init__.py +72 -91
- visidata/_input.py +263 -44
- visidata/_open.py +84 -29
- visidata/_types.py +22 -4
- visidata/_urlcache.py +17 -4
- visidata/aggregators.py +65 -25
- visidata/apps/__init__.py +0 -0
- visidata/apps/vdsql/__about__.py +8 -0
- visidata/apps/vdsql/__init__.py +5 -0
- visidata/apps/vdsql/__main__.py +27 -0
- visidata/apps/vdsql/_ibis.py +748 -0
- visidata/apps/vdsql/bigquery.py +61 -0
- visidata/apps/vdsql/clickhouse.py +53 -0
- visidata/apps/vdsql/setup.py +40 -0
- visidata/apps/vdsql/snowflake.py +67 -0
- visidata/apps/vgit/__init__.py +13 -0
- visidata/apps/vgit/__main__.py +3 -0
- visidata/apps/vgit/abort.py +23 -0
- visidata/apps/vgit/blame.py +76 -0
- visidata/apps/vgit/branch.py +153 -0
- visidata/apps/vgit/config.py +95 -0
- visidata/apps/vgit/diff.py +169 -0
- visidata/apps/vgit/gitsheet.py +161 -0
- visidata/apps/vgit/grep.py +37 -0
- visidata/apps/vgit/log.py +81 -0
- visidata/apps/vgit/main.py +55 -0
- visidata/apps/vgit/remote.py +57 -0
- visidata/apps/vgit/repos.py +71 -0
- visidata/apps/vgit/setup.py +37 -0
- visidata/apps/vgit/stash.py +69 -0
- visidata/apps/vgit/status.py +204 -0
- visidata/apps/vgit/statusbar.py +34 -0
- visidata/basesheet.py +59 -50
- visidata/canvas.py +251 -99
- visidata/choose.py +15 -11
- visidata/clean_names.py +29 -0
- visidata/clipboard.py +84 -18
- visidata/cliptext.py +220 -46
- visidata/cmdlog.py +89 -114
- visidata/color.py +142 -56
- visidata/column.py +134 -131
- visidata/ddw/input.ddw +74 -79
- visidata/ddw/regex.ddw +57 -0
- visidata/ddwplay.py +33 -14
- visidata/deprecated.py +77 -3
- visidata/desktop/visidata.desktop +7 -0
- visidata/editor.py +12 -6
- visidata/errors.py +5 -1
- visidata/experimental/__init__.py +0 -0
- visidata/experimental/diff_sheet.py +29 -0
- visidata/experimental/digit_autoedit.py +6 -0
- visidata/experimental/gdrive.py +89 -0
- visidata/experimental/google.py +37 -0
- visidata/experimental/gsheets.py +79 -0
- visidata/experimental/live_search.py +37 -0
- visidata/experimental/liveupdate.py +45 -0
- visidata/experimental/mark.py +133 -0
- visidata/experimental/noahs_tapestry/__init__.py +1 -0
- visidata/experimental/noahs_tapestry/tapestry.py +147 -0
- visidata/experimental/rownum.py +73 -0
- visidata/experimental/slide_cells.py +26 -0
- visidata/expr.py +8 -4
- visidata/extensible.py +32 -6
- visidata/features/__init__.py +0 -0
- visidata/features/addcol_audiometadata.py +42 -0
- visidata/features/addcol_histogram.py +34 -0
- visidata/features/canvas_save_svg.py +69 -0
- visidata/features/change_precision.py +46 -0
- visidata/features/cmdpalette.py +163 -0
- visidata/features/colorbrewer.py +363 -0
- visidata/{colorsheet.py → features/colorsheet.py} +17 -16
- visidata/features/command_server.py +105 -0
- visidata/features/currency_to_usd.py +70 -0
- visidata/{customdate.py → features/customdate.py} +2 -0
- visidata/features/dedupe.py +132 -0
- visidata/{describe.py → features/describe.py} +17 -15
- visidata/features/errors_guide.py +26 -0
- visidata/features/expand_cols.py +202 -0
- visidata/{fill.py → features/fill.py} +4 -2
- visidata/{freeze.py → features/freeze.py} +11 -6
- visidata/features/graph_seaborn.py +79 -0
- visidata/features/helloworld.py +10 -0
- visidata/features/hint_types.py +17 -0
- visidata/{incr.py → features/incr.py} +5 -0
- visidata/{join.py → features/join.py} +107 -53
- visidata/features/known_cols.py +21 -0
- visidata/features/layout.py +62 -0
- visidata/{melt.py → features/melt.py} +33 -21
- visidata/features/normcol.py +118 -0
- visidata/features/open_config.py +7 -0
- visidata/features/open_syspaste.py +18 -0
- visidata/features/ping.py +157 -0
- visidata/features/procmgr.py +208 -0
- visidata/features/random_sample.py +6 -0
- visidata/{regex.py → features/regex.py} +47 -31
- visidata/features/reload_every.py +55 -0
- visidata/features/rename_col_cascade.py +30 -0
- visidata/features/scroll_context.py +60 -0
- visidata/features/select_equal_selected.py +11 -0
- visidata/features/setcol_fake.py +65 -0
- visidata/{slide.py → features/slide.py} +75 -21
- visidata/features/sparkline.py +48 -0
- visidata/features/status_source.py +20 -0
- visidata/{sysedit.py → features/sysedit.py} +2 -1
- visidata/features/sysopen_mailcap.py +46 -0
- visidata/features/term_extras.py +13 -0
- visidata/{transpose.py → features/transpose.py} +5 -4
- visidata/features/type_ipaddr.py +73 -0
- visidata/features/type_url.py +11 -0
- visidata/{unfurl.py → features/unfurl.py} +9 -9
- visidata/{window.py → features/window.py} +2 -2
- visidata/form.py +50 -21
- visidata/freqtbl.py +81 -33
- visidata/fuzzymatch.py +414 -0
- visidata/graph.py +105 -33
- visidata/guide.py +180 -0
- visidata/help.py +75 -44
- visidata/hint.py +39 -0
- visidata/indexsheet.py +109 -0
- visidata/input_history.py +55 -0
- visidata/interface.py +58 -0
- visidata/keys.py +17 -16
- visidata/loaders/__init__.py +9 -0
- visidata/loaders/_pandas.py +61 -21
- visidata/loaders/api_airtable.py +70 -0
- visidata/loaders/api_bitio.py +102 -0
- visidata/loaders/api_matrix.py +148 -0
- visidata/loaders/api_reddit.py +306 -0
- visidata/loaders/api_zulip.py +249 -0
- visidata/loaders/archive.py +41 -7
- visidata/loaders/arrow.py +7 -7
- visidata/loaders/conll.py +49 -0
- visidata/loaders/csv.py +25 -7
- visidata/loaders/eml.py +3 -4
- visidata/loaders/f5log.py +1204 -0
- visidata/loaders/fec.py +325 -0
- visidata/loaders/fixed_width.py +3 -5
- visidata/loaders/frictionless.py +3 -3
- visidata/loaders/geojson.py +8 -5
- visidata/loaders/google.py +48 -0
- visidata/loaders/graphviz.py +4 -4
- visidata/loaders/hdf5.py +4 -4
- visidata/loaders/html.py +48 -10
- visidata/loaders/http.py +84 -30
- visidata/loaders/imap.py +20 -10
- visidata/loaders/jrnl.py +52 -0
- visidata/loaders/json.py +83 -29
- visidata/loaders/jsonla.py +74 -0
- visidata/loaders/lsv.py +15 -11
- visidata/loaders/mailbox.py +40 -0
- visidata/loaders/markdown.py +1 -3
- visidata/loaders/mbtiles.py +4 -5
- visidata/loaders/mysql.py +11 -13
- visidata/loaders/npy.py +7 -7
- visidata/loaders/odf.py +4 -1
- visidata/loaders/orgmode.py +428 -0
- visidata/loaders/pandas_freqtbl.py +14 -20
- visidata/loaders/parquet.py +62 -6
- visidata/loaders/pcap.py +3 -3
- visidata/loaders/pdf.py +4 -3
- visidata/loaders/png.py +19 -13
- visidata/loaders/postgres.py +9 -8
- visidata/loaders/rec.py +7 -3
- visidata/loaders/s3.py +342 -0
- visidata/loaders/sas.py +5 -5
- visidata/loaders/scrape.py +186 -0
- visidata/loaders/shp.py +6 -5
- visidata/loaders/spss.py +5 -6
- visidata/loaders/sqlite.py +68 -28
- visidata/loaders/texttables.py +1 -1
- visidata/loaders/toml.py +60 -0
- visidata/loaders/tsv.py +61 -19
- visidata/loaders/ttf.py +19 -7
- visidata/loaders/unzip_http.py +6 -5
- visidata/loaders/usv.py +1 -1
- visidata/loaders/vcf.py +16 -16
- visidata/loaders/vds.py +10 -7
- visidata/loaders/vdx.py +30 -5
- visidata/loaders/xlsb.py +8 -1
- visidata/loaders/xlsx.py +145 -25
- visidata/loaders/xml.py +6 -3
- visidata/loaders/xword.py +4 -4
- visidata/loaders/yaml.py +15 -5
- visidata/macos.py +1 -1
- visidata/macros.py +130 -41
- visidata/main.py +119 -94
- visidata/mainloop.py +101 -154
- visidata/man/parse_options.py +2 -2
- visidata/man/vd.1 +302 -147
- visidata/man/vd.txt +291 -151
- visidata/memory.py +3 -3
- visidata/menu.py +104 -423
- visidata/metasheets.py +59 -141
- visidata/modify.py +79 -23
- visidata/motd.py +3 -3
- visidata/mouse.py +137 -0
- visidata/movement.py +43 -35
- visidata/optionssheet.py +99 -0
- visidata/path.py +131 -43
- visidata/pivot.py +74 -47
- visidata/plugins.py +65 -192
- visidata/pyobj.py +50 -201
- visidata/rename_col.py +20 -0
- visidata/save.py +42 -20
- visidata/search.py +54 -10
- visidata/selection.py +84 -5
- visidata/settings.py +162 -24
- visidata/sheets.py +229 -257
- visidata/shell.py +51 -21
- visidata/sidebar.py +162 -0
- visidata/sort.py +11 -4
- visidata/statusbar.py +113 -104
- visidata/stored_list.py +43 -0
- visidata/stored_prop.py +38 -0
- visidata/tests/conftest.py +3 -3
- visidata/tests/test_cliptext.py +39 -0
- visidata/tests/test_commands.py +62 -7
- visidata/tests/test_edittext.py +2 -2
- visidata/tests/test_features.py +17 -0
- visidata/tests/test_menu.py +14 -0
- visidata/tests/test_path.py +13 -4
- visidata/text_source.py +53 -0
- visidata/textsheet.py +10 -3
- visidata/theme.py +44 -0
- visidata/themes/__init__.py +0 -0
- visidata/themes/ascii8.py +84 -0
- visidata/themes/asciimono.py +84 -0
- visidata/themes/light.py +17 -0
- visidata/threads.py +87 -39
- visidata/tuiwin.py +22 -0
- visidata/type_currency.py +22 -3
- visidata/type_date.py +31 -9
- visidata/type_floatsi.py +5 -1
- visidata/undo.py +18 -6
- visidata/utils.py +106 -23
- visidata/vdobj.py +28 -17
- visidata/windows.py +10 -0
- visidata/wrappers.py +9 -3
- visidata-3.0.data/data/share/applications/visidata.desktop +7 -0
- {visidata-2.11.dev0.data → visidata-3.0.data}/data/share/man/man1/vd.1 +302 -147
- {visidata-2.11.dev0.data → visidata-3.0.data}/data/share/man/man1/visidata.1 +302 -147
- visidata-3.0.data/scripts/vd2to3.vdx +9 -0
- {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/METADATA +13 -11
- visidata-3.0.dist-info/RECORD +257 -0
- {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/WHEEL +1 -1
- {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/entry_points.txt +0 -1
- visidata/layout.py +0 -44
- visidata/misc.py +0 -5
- visidata-2.11.dev0.dist-info/RECORD +0 -142
- /visidata/{repeat.py → features/repeat.py} +0 -0
- {visidata-2.11.dev0.data → visidata-3.0.data}/scripts/vd +0 -0
- {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/LICENSE.gpl3 +0 -0
- {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,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
|
+
''')
|