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,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
|
+
)
|