-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathvimdiff
executable file
·307 lines (276 loc) · 10.7 KB
/
vimdiff
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
#!/bin/sh
help() {
if [ "$full_help" = 1 ] || [ ! -t 1 ] || [ "$(tput lines)" -gt 99 ]; then
# Run `vim --help`, update the name, print after line 2
usage=$("$vim" --help 2>$dn |awk -vi=" $self " '{ sub(" vim ", i) } NR > 2')
tall=1
fi
cat <</help
Drop-in replacement for [g]vimdiff.
Given one file, compares to git, SyncThing, snapshots, or else backup names.
${usage:-Usage: $self [arguments] [file ...]}
Arguments recognized by vimdiff-auto wrapper (@adamhotep misc-scripts):
--full-help Include vim command-line help$([ -n "$tall" ] && echo "
--help Display this command-line help and exit
--test, --noop Report the vim command but do not run it
--version Display vim & vimdiff-auto version info and exit")
--exec <path> VIM executable, currently \`$vim\`
--snapshot <path> Snapshot directory (absolute or relative to the file).
ZFS snapshots are auto-detected UNLESS this path exists.
Non-ZFS snapshots (e.g. for NetApps) are experimental.
Currently \`$snapshot\`
--syncthing <path> SyncThing versions directory (relative or absolute).
Currently \`$syncthing\`
Install ahead of vimdiff in your \$PATH like /usr/local/bin/vimdiff and create a
symlink named gvimdiff that points to it (if you have gvim installed).
/help
version
}
# Debug mode is supported by running as `sh -x /path/to/this/vimdiff`
# You can alternatively use bash or zsh for ~better xtrace information.
version() { cat <</version
Wraps $(vim_version "vim (version unknown, '$vim --help' failed)")
Part of misc-scripts: https://github/adamhotep/misc-scripts
vimdiff-auto 2.2.20241025.0 copyright 2009+ by Adam Katz, GPLv3
/version
exit
}
# Usage: vim_version FALLBACK_TEXT
# Parse `vim --help` and get the version (line 1) or else show FALLBACK_TEXT
vim_version() {
# GNU, BSD, and Busybox all support `grep -m NUMBER`
"$vim" --help 2>$dn |grep -m1 . || echo "$*"
}
# Version 1.x (`vimdiff+cvs`) could also automatically traverse CVS & Subversion
# repositories, but that functionality has been removed in favor of git.
#
# While this script can properly invoke git itself, you can additionally put
# `vimdiff = difftool -y -t vimdiff` in the `[alias]` section of ~/.gitconfig
# (and the same for `gvimdiff`) to be able to run `git vimdiff bar.txt`
# Escape glob characters.
escape_glob() { echo "$*" |sed 's/[\]\[\\*?]/\\&/g'; }
# We need to do some crazy escaping for the nested call to git.
# Escape single-quotes; `I'm happy` -> `'I'\''m happy'`
# This could probably use some more security vetting.
escape_squot() { gitcmd="$gitcmd '$(echo "$*" |sed "s/'/'\\''/g")'"; }
# Usage: differ OLD FILE
# Prints OLD and returns true when OLD is a nonzero file NOT identical to FILE
differ() {
if [ -f "$1" ] && [ -s "$1" ] && ! diff -q "$@" >$dn; then
echo "$1"
return 0
fi
return 1
}
# Usage: git_diff [DIR] FILE
# If git tracks FILE, vimdiff it vs git HEAD (even if there are no differences)
git_diff() {
local owd="$PWD"
if ! command -v git >$dn; then
: "git is not installed, skipping git diff"
return 1
fi
: "Seeking a git revision for <$1>"
if [ "$PWD" != "${1%/*}" ]; then cd "${1%/*}"; fi
#if git ls-files --error-unmatch "$1" 2>$dn; then # git tracked?
git diff --quiet "$last" 2>$dn
case $? in
( 0 ) echo "$self: git repo found but <$1> is unchanged or untracked" ;;
( 1 ) : "Found a git revision of <$1> that differs"
exec git difftool --no-prompt --extcmd="$gitcmd" -- "$1" ;;
( * ) : "Failed to find a git respository for <$1>" ;;
esac
if [ "$PWD" != "${1%/*}" ]; then cd "$owd"; fi
}
# Usage: syncthing_version FILE
# Find a potential SyncThing version of FILE
syncthing_version() {
: "Seeking a SyncThing version for <$1>"
local dir="$1" old fn fn_ver ext timestamp
while dir="${dir%/*}" && [ -d "$dir" ]; do
if [ -d "$dir/$syncthing" ]; then
old="$dir/$syncthing/${last#$dir/}" # trash can versioning
differ "$old" "$1" && return 0
fn="${last##*/}"
fn_ver="$(escape_glob "$fn")"
ext="${fn_ver##*.}"
timestamp="~$ymd-$hms"
if [ "$ext" != "$fn_ver" ]; then # we have an extension
fn_ver="${fn_ver%.$ext}$timestamp.$ext"
else
fn_ver="$fn_ver$timestamp"
fi
find_eligible "$1" "$dir/$syncthing" -name "$fn_ver" && return 0
break # for speed & simplicity, assume there are no nested $syncthing
fi
done
return 1
}
# Usage: find_eligible FILE PATH [PATH...] [FIND_OPTIONS]
# Report the most recent eligible file in the given PATHs. SUFFIX may be a glob.
find_eligible() {
local file="$1" name or= old
shift # remove the FILE
# GNU, BSD, and Busybox `find` all support -maxdepth, -mindepth, and -print0
# even though they're not in the POSIX standard. Ditto for `xargs -0`.
# The awk call makes the list unique without changing the order.
# I believe it's `find` that is super slow on ZFS snapshots.
find "$@" -type f -size +0 -print0 2>$dn |xargs -0 ls -td |awk '!seen[$0]++' \
|while IFS= read -r old; do differ "$old" "$file" && return 0; done \
|grep ^ # the `while` loop was in a subshell. this sets the return code.
}
# Usage: snapshot_version FILE [FIND_OPTIONS]
# Should support ZFS, NetApps, Infinidat, etc. Only ZFS & Infinidat are tested.
# Warning, ZFS is slow if there are lots of recent and identical snapshots.
# Expects a file of the same name as FILE 1-2 levels below the snapshot dir.
# Auto-senses the ZFS snapshot dir.
snapshot_version() {
: "Seeking a snapshot for <$1>"
local dir zfs= zfsdir= old= parent="${1%/*}/"
case "$snapshot" in
( /* ) dir="$snapshot" ;;
( * ) dir="$parent$snapshot" ;;
esac
if [ ! -d "$dir" ]; then
# *if* on a ZFS filesystem, set $zfs to the file's ZFS mount point
# GNU, BSD, and Busybox all support `df -T`. Busybox lacks `df -t type`.
zfs=$(df -T "$1" |awk '$2=="zfs" { gsub(/.*%[[:space:]]+\//, "/"); print }')
dir="$zfs/.zfs/snapshot"
zfsdir="$dir/${parent#$zfs/}"
zfsdir="${zfsdir%/}"
if [ "$zfsdir" = "$dir" ]; then zfsdir=""; fi
fi
if [ -d "$dir" ]; then
: "Seeking newest different snapshot of <${1#$zfs/}> in <$dir>"
find_eligible "$@" "$dir" ${zfs:+"$zfsdir"} \
-name "$(escape_glob "${1##*/}")" -mindepth 1 -maxdepth 2
return $?
fi
return 1
}
# Usage: backup_copy FILE [FIND_OPTIONS]
# Support common backup file names
backup_copy() {
: "Seeking a backup filename for <$1>"
local file="$1" base="$(escape_glob "${1##*/}")" ext or=
shift
set -- "$file" "${file%/*}" -maxdepth 1 "$@" '('
# Here is the list of suffix patterns we accept. Note the non-suffix later.
for ext in '~' .backup .bak .new .old .orig .rej .tmp \
".$ymd" ".$ymd$hms" ".$ymd[-_.]$hms" \
'.*[-.]dist' '.*[-.]new' '.*[-.]old' '.*[-.]tmp' '.rpm[nos][era][wgv]*'
do
set -- "$@" $or -name "$base$ext"
or='-o'
done
# add `#file#` (not just a suffix) and the end-parenthesis
set -- "$@" $or -name "#$base#" ')'
find_eligible "$@"
}
# Defaults
self="${0##*/}"
snapshot="${SNAPSHOT_DIR:-.snapshot}"
syncthing=".stversions"
default_vim="vim"
vim="$default_vim"
# Constants and initializations
dn='/dev/null'
d='[0-9]'
ymd="20$d$d[01]$d[0-3]$d"
hms="[012]$d[0-5]$d[0-6]$d"
last=""
penultimate=""
reset=0
opt=1
skip=0
full_help=0
tall=""
split=vsp
gitcmd="$0 --nofork"
noop=""
# special case for just -V (if you actually want that, use `-V10`)
if [ "$*" = -V ]; then version; fi
# Loop through the arguments, find -h/--help, note the last two files (if any).
# This is a little messy because vim accepts options EVERYWHERE except after --
for arg in "$@"; do
: "Parsing arg <$arg>"
if [ "$reset" = 0 ]; then reset=1; set --; fi # reset the $@ array
if [ "$opt" = 1 ]; then
if [ "$skip" != 0 ]; then
case "$skip" in
( exec ) vim="$arg" ;; # alternate vim executable
( snapshot ) snapshot="$arg" ;; # Snapshot directory
( syncthing ) syncthing="$arg" ;; # SyncThing versions directory
( 1 ) escape_squot "$arg"; set -- "$@" "$arg" ;; # vim option arg
esac
skip=0
continue
elif [ "$arg" != "${arg#[-+]?}" ]; then
push=
case "$arg" in
# options with required arguments: cue skipping
( --cmd | -[!-]*[tqcSiPrsTuUwW] | -[tqcSiPrsTuUwW] \
| --gui-dialog-file | --log | --remote-expr | --remote-send \
| --servername | --socketid | --startuptime | --windowid ) skip=1 ;;
# options that we intercept and use locally
( -- ) opt=0 ;;
( --exec*=* ) vim="${arg#*=}" ;;
( --exec ) skip=exec ;;
( --full-help ) full_help=1; help ;;
( --help | -[h?]* | -[!-]*[h?]* ) help ;;
( --noop | --test ) noop="echo" gitcmd="echo ${gitcmd#echo }" ;;
( --snap*=* ) snapshot="${arg#*=}" ;;
( --snap* ) skip=snapshot ;;
( --syncthing*=* ) syncthing="${arg#*=}" ;;
( --syncthing* ) skip=syncthing ;;
( --version ) version ;;
# other vim options
( -o* ) push=1 split=sp ;; # horizontal split
( * ) push=1 ;;
esac
if [ "$skip" = 1 ] || [ "$push" = 1 ]; then
escape_squot "$arg"
set -- "$@" "$arg"
fi
continue
fi
fi
if [ -e "$last" ]; then penultimate="$last"; fi
if [ -e "$arg" ]; then last="$arg"; fi
set -- "$@" "$arg"
done
# no file arguments: open a diff between two new files
if [ ! -e "$last" ]; then
set -- "$@" +"$split|enew|diffthis"
# Just one file
elif [ -z "$penultimate" ]; then
last=$(readlink -f "$last") # we want the true path, not a symlink ...right?
git_diff "$last" # If there's a git revision to diff, do that and exit
# we're taking full advantage of vim's ability to take options anywhere
# and avoiding the need to inject $penultimate immediately before $last
if penultimate="$(syncthing_version "$last")"; then
: "Found a syncthing version of <$last>"
set -- "$penultimate" "$@"
elif penultimate="$(snapshot_version "$last")"; then
: "Found a snapshot copy of <$last>"
set -- "$penultimate" "$@"
elif penultimate="$(backup_copy "$last")"; then
: "Found a backup copy of <$last>"
set -- "$penultimate" "$@"
else
apology='echo "(Could not find a git, snapshot, or backup copy to diff)"'
set -- "$@" +"vsp|enew|diffthis|$apology"
fi
fi
if [ "$vim" != "$default_vim" ]; then
: "Running nondefault vim <$vim> rather than guessing GUI"
: "Consider using \`-g\` force vim to run the GUI"
exec $noop "$vim" -d "$@"
fi
undef() { echo "Undefined script name '$self', guessing $* vim" >&2; }
case "$self" in
( vimdiff* ) exec $noop "$vim" -d "$@" ;;
( *vimdiff* ) exec $noop "$vim" -gd "$@" ;;
( [gkwx]* ) undef graphical; exec $noop "$vim" -gd "$@" ;;
( * ) undef console; exec $noop "$vim" -d "$@" ;;
esac