Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,35 @@ positional arguments:
pathB

options:
-h, --help show this help message and exit
--vim VIM vim command to run
--onlydiffs only open files where there is a diff
-h, --help show this help message and exit
--vim VIM vim command to run
--exclude path_glob1,path_glob2 comma separated list of files/folders to exclude
--match regex1,regex2 comma separated list of regular expressions to limit the scope to only paths matching one of the expressions (e.g. 'components.*,http')
--git shortcut to add glob '**/.git' to exclusion list
--onlydiffs only open files where there is a diff
```

# Usage example

```bash
# Show diff of files in two directories
vimtabdiff.py path/to/dirA path/to/dirB

# Show diff of files in two directories, excluding folder matching '**/.git' fileglob
vimtabdiff.py --git path/to/dirA path/to/dirB

# Show diff of files in two directories, excluding .git folder and *.pyc files in top level directories
vimtabdiff.py --exclude '**/.git,*.pyc' path/to/dirA path/to/dirB

# Show diff of files in two directories, only showing files with diffs
vimtabdiff.py --onlydiffs path/to/dirA path/to/dirB

# Show diff of files in two directories, only showing files with full paths matching any of the regexes
vimtabdiff.py --match 'components.*,http' path/to/dirA path/to/dirB
```

The `--match` option is useful when you want to limit the scope of the diff to only certain files or directories. For example, if you have a large project and you only want to see changes in the `components` directory or files that contain `http`, you can use this option. It's applied to the full path of the files, so you can use it to match directories as well, after applying the `--exclude` option.

## Relevant vim tips

* `gt` → Go to next tab
Expand Down
60 changes: 43 additions & 17 deletions vimtabdiff.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
#!/usr/bin/python3
#!/usr/bin/env python3

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import os
import sys
import argparse
import itertools
import tempfile
import subprocess
import shlex
import re
from pathlib import Path
from typing import TypeVar
from collections.abc import Iterator, Callable
Expand All @@ -29,21 +31,23 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("pathA", type=Path)
parser.add_argument("pathB", type=Path)
parser.add_argument("--vim", help="vim command to run", default="vim")
parser.add_argument(
"--onlydiffs", help="only open files where there is a diff", action="store_true"
)
parser.add_argument("--exclude", help="comma separated list of files/folders to exclude (e.g. .git)", default=None)
parser.add_argument("--match", help="comma separated list of regular expressions to limit the scope to only paths matching one of the expressions (e.g. 'components.*,http')", default=None)
parser.add_argument("--git", help="add **/.git to exclusion list", action="store_true")
parser.add_argument("--onlydiffs", help="only open files where there is a diff", action="store_true")
return parser.parse_args()


def get_dir_info(dirpath: Path | None) -> tuple[list[Path], list[Path]]:
def get_dir_info(dirpath: Path | None, exclude: list[str]) -> tuple[list[Path], list[Path]]:
if not dirpath:
return [], []
dirs, files = [], []
for p in dirpath.iterdir():
if p.is_dir():
dirs.append(p)
else:
files.append(p)
if not any( (p.full_match(e) for e in exclude) ):
if p.is_dir():
dirs.append(p)
else:
files.append(p)
return dirs, files


Expand All @@ -66,33 +70,51 @@ def get_pairs(aPaths: list[Path],

def get_file_pairs(
a: Path | None,
b: Path | None) -> Iterator[tuple[Path | None, Path | None]]:
aDirs, aFiles = get_dir_info(a)
bDirs, bFiles = get_dir_info(b)
b: Path | None,
exclude: list[str]) -> Iterator[tuple[Path | None, Path | None]]:
aDirs, aFiles = get_dir_info(a, exclude)
bDirs, bFiles = get_dir_info(b, exclude)
yield from get_pairs(aFiles, bFiles)
for aDir, bDir in get_pairs(aDirs, bDirs):
yield from get_file_pairs(aDir, bDir)
yield from get_file_pairs(aDir, bDir, exclude)


def main() -> None:
args = parse_args()

excludeList = []
if args.git:
excludeList.append('**/.git')

if args.exclude:
for p in args.exclude.split(','):
excludeList.append(p)

matchList = []
if args.match:
matchList = args.match.split(',')

vimCmdFile = tempfile.NamedTemporaryFile(mode='w', delete=False)
haveFiles = False
with vimCmdFile:
cmds = f"""
let s:spr = &splitright
set splitright
"""
print(cmds, file=vimCmdFile)
for a, b in get_file_pairs(args.pathA, args.pathB):
aPath = a.resolve() if a else os.devnull
bPath = b.resolve() if b else os.devnull
for a, b in get_file_pairs(args.pathA, args.pathB, excludeList):
aPath = a.resolve() if a else args.pathA.joinpath(b.relative_to(args.pathB)).resolve() # os.devnull
bPath = b.resolve() if b else args.pathB.joinpath(a.relative_to(args.pathA)).resolve() # os.devnull
if (
args.onlydiffs
and a and b
and open(aPath, mode="rb").read() == open(bPath, mode="rb").read()
):
continue
if matchList and not any(((re.search(m, str(aPath), re.IGNORECASE)) or (re.search(m, str(bPath), re.IGNORECASE)) for m in matchList)):
continue
print(f"tabedit {aPath} | vsp {bPath}", file=vimCmdFile)
haveFiles = True
cmds = f"""
let &splitright = s:spr
tabdo windo :1
Expand All @@ -101,8 +123,12 @@ def main() -> None:
tabfirst | tabclose
call delete("{vimCmdFile.name}")
"""

print(cmds, file=vimCmdFile)
subprocess.run(shlex.split(args.vim) + ["-S", vimCmdFile.name])
if haveFiles:
subprocess.run(shlex.split(args.vim) + ["-S", vimCmdFile.name])
else:
print(f"no files for comparision selected")


if __name__ == '__main__':
Expand Down