Commit 386557e646a97464c713aa6ee607008c85f37aad
1 parent
aaf9bcef
Added support for git filter and inflate --all.
Showing
2 changed files
with
139 additions
and
54 deletions
blimp.nim
1 | -import md5, os, osproc, parseopt2, strutils, parsecfg, streams, lapp, subexes | |
1 | +import md5, os, osproc, parseopt2, strutils, parsecfg, streams, lapp, subexes, tables | |
2 | 2 | |
3 | 3 | # blimp is a little utility program for handling large files |
4 | 4 | # in git repositories. Its inspired by git-fat and s3annex |
... | ... | @@ -11,7 +11,7 @@ import md5, os, osproc, parseopt2, strutils, parsecfg, streams, lapp, subexes |
11 | 11 | # Use "blimp i mybigfile" to inflate it back to original size. |
12 | 12 | # |
13 | 13 | # When deflated the file only has: |
14 | -# "hash:" <filename> + <an md5sum> | |
14 | +# "blimphash:" <filename> + <an md5sum> | |
15 | 15 | # ...inside it. |
16 | 16 | # |
17 | 17 | # The file is copied over to a local dir: |
... | ... | @@ -30,14 +30,14 @@ import md5, os, osproc, parseopt2, strutils, parsecfg, streams, lapp, subexes |
30 | 30 | |
31 | 31 | const |
32 | 32 | versionMajor* = 0 |
33 | - versionMinor* = 2 | |
34 | - versionPatch* = 1 | |
33 | + versionMinor* = 3 | |
34 | + versionPatch* = 0 | |
35 | 35 | versionAsString* = $versionMajor & "." & $versionMinor & "." & $versionPatch |
36 | 36 | |
37 | 37 | var |
38 | 38 | blimpStore, remoteBlimpStore, uploadCommandFormat, downloadCommandFormat, deleteCommandFormat, rsyncPassword, blimpVersion: string = nil |
39 | 39 | homeDir, currentDir, gitRootDir: string |
40 | - verbose, stdio: bool | |
40 | + verbose, stdio, inflateAll: bool | |
41 | 41 | stdinContent: string = nil |
42 | 42 | |
43 | 43 | let |
... | ... | @@ -72,10 +72,11 @@ delete = "rsync --password-file $3/.blimp.pass -dv --delete --existing --ignore- |
72 | 72 | # version = 0.2 |
73 | 73 | """ |
74 | 74 | |
75 | -# Find git root dir or nil | |
76 | -proc gitRoot(): string = | |
75 | + | |
76 | + | |
77 | +proc cmd(cmd: string): string = | |
77 | 78 | try: |
78 | - let tup = execCmdEx("git rev-parse --show-toplevel") | |
79 | + let tup = execCmdEx(cmd) | |
79 | 80 | if tup[1] == 0: |
80 | 81 | result = strip(tup[0]) |
81 | 82 | else: |
... | ... | @@ -83,6 +84,29 @@ proc gitRoot(): string = |
83 | 84 | except: |
84 | 85 | result = nil |
85 | 86 | |
87 | +# Find git root dir or nil | |
88 | +proc gitRoot(): string = | |
89 | + cmd("git rev-parse --show-toplevel") | |
90 | + | |
91 | +# Git config | |
92 | +proc gitConfigSet(key, val: string) = | |
93 | + discard cmd("git config " & $key & " " & $val) | |
94 | + | |
95 | +proc gitConfigGet(key: string): string = | |
96 | + cmd("git config --get " & $key) | |
97 | + | |
98 | +# Set blimp filter | |
99 | +proc setBlimpFilter() = | |
100 | + gitConfigSet("filter.blimp.clean", "\"blimp -s d %f\"") | |
101 | + gitConfigSet("filter.blimp.smudge", "\"blimp -s i %f\"") | |
102 | + | |
103 | + | |
104 | +# Ensure the filter is set | |
105 | +proc ensureBlimpFilter() = | |
106 | + if gitConfigGet("filter.blimp.clean") != "": | |
107 | + setBlimpFilter() | |
108 | + | |
109 | + | |
86 | 110 | # Simple expansion of %home%, %cwd% and %gitroot% |
87 | 111 | proc expandDirs(templ: string): string = |
88 | 112 | result = templ.replace("%home%", homeDir) |
... | ... | @@ -96,8 +120,7 @@ proc parseConfFile(filename: string) = |
96 | 120 | var f = newFileStream(filename, fmRead) |
97 | 121 | if f != nil: |
98 | 122 | if verbose: echo "Reading config: " & filename |
99 | - var p: CfgParser | |
100 | - | |
123 | + var p: CfgParser | |
101 | 124 | open(p, f, filename) |
102 | 125 | while true: |
103 | 126 | var e = next(p) |
... | ... | @@ -200,8 +223,8 @@ proc deleteFromBlimpStore(blimpFilename, filename: string) = |
200 | 223 | remoteDeleteFile(blimpFilename) |
201 | 224 | |
202 | 225 | proc blimpFileNameFromString(line: string): string = |
203 | - let hashline = split(strip(line), {':'}) | |
204 | - if hashline[0] == "hash": | |
226 | + let hashline = split(strip(line), ':') | |
227 | + if hashline[0] == "blimphash": | |
205 | 228 | result = hashline[1] |
206 | 229 | else: |
207 | 230 | result = nil |
... | ... | @@ -236,11 +259,18 @@ proc deflate(filename: string) = |
236 | 259 | blimpFilename = computeBlimpFilename(filename) |
237 | 260 | copyToBlimpStore(filename, blimpFilename) |
238 | 261 | if stdio: |
239 | - write(stdout, "hash:" & blimpFilename) | |
262 | + write(stdout, "blimphash:" & blimpFilename) | |
240 | 263 | else: |
241 | - writeFile(filename, "hash:" & blimpFilename) | |
264 | + writeFile(filename, "blimphash:" & blimpFilename) | |
242 | 265 | if verbose: echo("\t" & filename & " deflated.") |
243 | 266 | |
267 | +# Iterator over all deflated files in the git clone | |
268 | +iterator allDeflated() = | |
269 | + let filenames = cmd("git ls-files " & gitRootDir).split('\l') | |
270 | + for fn in filenames: | |
271 | + if not blimpFilename(fn).isNil: | |
272 | + yield fn | |
273 | + | |
244 | 274 | # Parse out hash from hash stub and copy back original content from blimpStore. |
245 | 275 | proc inflate(filename: string) = |
246 | 276 | if verbose: echo "Inflating " & filename |
... | ... | @@ -290,41 +320,45 @@ proc dumpConfig() = |
290 | 320 | echo "\tblimpVersion: " & $blimpVersion |
291 | 321 | echo "\n" |
292 | 322 | |
293 | -let help = """ | |
323 | +let synopsis = """ | |
294 | 324 | blimp [options] <command> <filenames...> |
295 | 325 | -h,--help Show this |
296 | 326 | --version Show version of blimp |
297 | 327 | -v,--verbose Verbosity, only works without -s |
298 | - -s,--stdio If given, use stdin/stdout for content. | |
328 | + -i,--init Set blimp filter in git config | |
329 | + ---------- | |
299 | 330 | <command> (string) (d)eflate, (i)nflate, remove |
300 | - <filenames> (string...) One or more filepaths to inflate/deflate | |
301 | - | |
331 | + -a,--all Operate on all deflated files in clone | |
332 | + ---------- | |
333 | + -s,--stdio If given, use stdin/stdout for content. | |
334 | + <filenames> (string...) One or more filepaths to inflate/deflate | |
335 | +""" | |
336 | +let help = """ | |
302 | 337 | blimp is a little utility program for handling large files |
303 | 338 | in git repositories. Its inspired by git-fat and s3annex |
304 | 339 | but doesn't rely on S3 for storage - it uses rsync like git-fat. |
305 | - It is a single binary without any dependencies. | |
340 | + It is a single binary without any dependencies. Its not as advanced | |
341 | + as git-fat but basically does the same thing. | |
306 | 342 | |
307 | 343 | Manual use: |
308 | 344 | |
309 | - Use "blimp d mybigfile" to deflate it before commit. | |
310 | - Use "blimp i mybigfile" to inflate it back to original size. | |
345 | + Use "blimp d mybigfile" to deflate a file, typically before commit. | |
346 | + Use "blimp i mybigfile" to inflate it back to original content. | |
311 | 347 | |
312 | 348 | When deflated the file only has: |
313 | - "hash:" <filename> "-" <md5sum> | |
349 | + "blimphash:" <filename> "-" <md5sum> | |
314 | 350 | ...inside it. |
315 | 351 | |
316 | - Deflate is run before you add the big file to the index for committing. | |
317 | - Deflate will replace the file contents with a hash, and copy the | |
318 | - real content to your local blimpstore: | |
352 | + Deflate also copies the real content to your local blimpstore: | |
319 | 353 | |
320 | - "blimpstore"/<filename>-<md5sum> | |
354 | + <blimpstore>/<filename>-<md5sum> | |
321 | 355 | |
322 | - ...and if configured also upload it to "remote", using rsync. | |
356 | + ...and if configured also uploads it to "remote", using rsync. | |
323 | 357 | |
324 | 358 | Configuration is in these locations in order: |
325 | 359 | |
326 | 360 | ./.blimp.conf |
327 | - "gitroot"/.blimp.conf | |
361 | + <gitroot>/.blimp.conf | |
328 | 362 | ~/<blimpstore>/.blimp.conf |
329 | 363 | ~/.blimp.conf |
330 | 364 | |
... | ... | @@ -337,18 +371,40 @@ let help = """ |
337 | 371 | synced with a master rsync repository that is typically shared. |
338 | 372 | |
339 | 373 | Inflate will bring back the original content by copying from |
340 | - your local blimpstore, and if its not there, first downloading from the remote. | |
374 | + your local blimpstore, and if its not there, first download from the remote. | |
341 | 375 | Use this whenever you need to work/edit the big file - in order to get |
342 | 376 | its real content. |
343 | 377 | |
344 | 378 | The filenames given are all processed. If -s is used content is processed via |
345 | 379 | stdin/stdout and only one filename can be passed. This is used when running blimp |
346 | - via a git filter (smudge/clean). | |
380 | + via a git filter (smudge/clean), see below. | |
347 | 381 | |
348 | 382 | The remove command (no single character shortcut) will remove the file(s) content |
349 | 383 | both from the local blimpstore and from the remote. This only removes |
350 | 384 | the current content version, not older versions. The file itself is first |
351 | 385 | inflated, if needed, and not deleted. This only "unblimps" the file. |
386 | + | |
387 | + In order to have blimp work automatically you can: | |
388 | + | |
389 | + * Create a .gitattributes file with lines like: | |
390 | + *.png filter=blimp -text | |
391 | + | |
392 | + * Configure blimp as a filter by running: | |
393 | + git config filter.blimp.clean "blimp -s d %f" | |
394 | + git config filter.blimp.smudge "blimp -s i %f" | |
395 | + | |
396 | + The above git config can be done by running "blimp --init". | |
397 | + | |
398 | + When the above is done (per clone) git will automatically run blimp deflate | |
399 | + just before committing and blimp inflate when operations are done. | |
400 | + | |
401 | + This means that if you clone a git repository that already has a .gitattributes | |
402 | + file in it that uses the blimp filter, then you should do: | |
403 | + | |
404 | + blimp --init inflate --all | |
405 | + | |
406 | + This will configure the blimp filter and also find and inflate all deflated | |
407 | + files throughout the clone. | |
352 | 408 | """ |
353 | 409 | |
354 | 410 | ################################ main ##################################### |
... | ... | @@ -359,9 +415,10 @@ currentDir = getCurrentDir() |
359 | 415 | gitRootDir = gitRoot() |
360 | 416 | |
361 | 417 | # Using lapp to get args, on parsing failure this will show usage automatically |
362 | -var args = parse(help) | |
418 | +var args = parse(synopsis) | |
363 | 419 | verbose = args["verbose"].asBool |
364 | 420 | stdio = args["stdio"].asBool |
421 | +inflateAll = args["all"].asBool | |
365 | 422 | |
366 | 423 | # Can't do verbose with -s, that messes up stdout, |
367 | 424 | # read in all of stdin once and for all |
... | ... | @@ -376,7 +433,7 @@ if stdio: |
376 | 433 | |
377 | 434 | # Parse configuration files, may shadow and override each other |
378 | 435 | parseConfFile(currentDir / ".blimp.conf") |
379 | -if not gitRootDir.isNil: | |
436 | +if not gitRootDir.isNil and gitRootDir != currentDir: | |
380 | 437 | parseConfFile(gitRootDir / ".blimp.conf") |
381 | 438 | |
382 | 439 | # If we haven't gotten a blimpstore yet, we set a default one |
... | ... | @@ -387,35 +444,47 @@ if existsDir(blimpStore): |
387 | 444 | parseConfFile(blimpStore / ".blimp.conf") |
388 | 445 | parseConfFile(homeDir / ".blimp.conf") |
389 | 446 | |
447 | +# Let's just show what we have :) | |
390 | 448 | if verbose: dumpConfig() |
391 | 449 | |
392 | 450 | # These two are special, they short out |
393 | -if args.showHelp: quit(help) | |
451 | +if args.showHelp: quit(synopsis & help) | |
394 | 452 | if args.showVersion: quit("blimp version: " & versionAsString) |
395 | 453 | |
396 | 454 | # Check blimpVersion |
397 | 455 | if not blimpVersion.isNil and blimpVersion != versionAsString: |
398 | 456 | quit("Wrong version of blimp, configuration wants: " & blimpVersion) |
399 | 457 | |
458 | +# Should we install the filter? | |
459 | +if args["init"].asBool: | |
460 | + ensureBlimpFilter() | |
461 | + if verbose: echo("Installed blimp filter") | |
462 | + | |
400 | 463 | let command = args["command"].asString |
401 | -let filenames = args["filenames"].asSeq | |
464 | +var filenames: seq[PValue] | |
465 | +if args.hasKey("filenames"): | |
466 | + filenames = args["filenames"].asSeq | |
402 | 467 | |
403 | 468 | # Make sure the local blimpstore is setup. |
404 | 469 | setupBlimpStore() |
405 | 470 | |
406 | - | |
407 | 471 | # Do the deed |
408 | -if command == "d" or command == "deflate": | |
409 | - for fn in filenames: | |
410 | - deflate(fn.asString) | |
411 | -elif command == "i" or command == "inflate": | |
412 | - for fn in filenames: | |
413 | - inflate(fn.asString) | |
414 | -elif command == "remove": | |
415 | - for fn in filenames: | |
416 | - remove(fn.asString) | |
417 | -else: | |
418 | - quit("Unknown command, only (d)eflate or (i)inflate are valid.", 6) | |
472 | +if command != "": | |
473 | + if command == "d" or command == "deflate": | |
474 | + for fn in filenames: | |
475 | + deflate(fn.asString) | |
476 | + elif command == "i" or command == "inflate": | |
477 | + if inflateAll: | |
478 | + for fn in allDeflated(): | |
479 | + inflate(fn) | |
480 | + else: | |
481 | + for fn in filenames: | |
482 | + inflate(fn.asString) | |
483 | + elif command == "remove": | |
484 | + for fn in filenames: | |
485 | + remove(fn.asString) | |
486 | + else: | |
487 | + quit("Unknown command, only (d)eflate or (i)inflate are valid.", 6) | |
419 | 488 | |
420 | 489 | # All good |
421 | 490 | quit(0) | ... | ... |
lapp.nim
... | ... | @@ -17,6 +17,8 @@ type |
17 | 17 | trange |
18 | 18 | telipsis |
19 | 19 | tchar |
20 | + toption | |
21 | + tdivider | |
20 | 22 | |
21 | 23 | proc thisChar(L: PLexer):char = L.str[L.idx] |
22 | 24 | proc next(L: PLexer) = L.idx += 1 |
... | ... | @@ -35,9 +37,15 @@ proc get(L: PLexer; t: var TLexType): string = |
35 | 37 | t = tchar |
36 | 38 | case c |
37 | 39 | of '-': # '-", "--" |
40 | + t = toption | |
38 | 41 | if thisChar(L) == '-': |
39 | 42 | result.add('-') |
40 | 43 | next(L) |
44 | + if thisChar(L) == '-': # "---..." | |
45 | + t = tdivider | |
46 | + result.add('-') | |
47 | + while thisChar(L) == '-': | |
48 | + next(L) | |
41 | 49 | of Letters: # word |
42 | 50 | t = tword |
43 | 51 | while thisChar(L) in Letters: |
... | ... | @@ -112,14 +120,13 @@ proc intValue(v: int): PValue = PValue(kind: vInt, asInt: v) |
112 | 120 | proc floatValue(v: float): PValue = PValue(kind: vFloat, asFloat: v) |
113 | 121 | |
114 | 122 | proc seqValue(v: seq[PValue]): PValue = PValue(kind: vSeq, asSeq: v) |
115 | - | |
116 | -const MAX_FILES = 30 | |
117 | 123 | |
118 | 124 | type |
119 | 125 | PSpec = ref TSpec |
120 | 126 | TSpec = object |
121 | 127 | defVal: string |
122 | 128 | ptype: string |
129 | + group: int | |
123 | 130 | needsValue, multiple, used: bool |
124 | 131 | var |
125 | 132 | progname, usage: string |
... | ... | @@ -134,6 +141,7 @@ proc parseSpec(u: string) = |
134 | 141 | var |
135 | 142 | L: PLexer |
136 | 143 | tok: string |
144 | + groupCounter: int | |
137 | 145 | k = 1 |
138 | 146 | |
139 | 147 | let lines = u.splitLines |
... | ... | @@ -173,7 +181,8 @@ proc parseSpec(u: string) = |
173 | 181 | k += 1 |
174 | 182 | tok = L.get |
175 | 183 | if tok != ">": fail("argument must be enclosed in <...>") |
176 | - | |
184 | + elif tok == "---": # divider | |
185 | + inc(groupCounter) | |
177 | 186 | if getnext: tok = L.get |
178 | 187 | if tok == ":": # allowed to have colon after flags |
179 | 188 | tok = L.get |
... | ... | @@ -205,8 +214,8 @@ proc parseSpec(u: string) = |
205 | 214 | defValue = "false" |
206 | 215 | |
207 | 216 | if name != nil: |
208 | - # echo("Param: " & name & " type: " & $ftype & " needsvalue: " & $(ftype != "bool") & " default: " & $defValue & " multiple: " & $multiple) | |
209 | - let spec = PSpec(defVal:defValue, ptype: ftype, needsValue: ftype != "bool",multiple:multiple) | |
217 | + #echo("Param: " & name & " type: " & $ftype & " group: " & $groupCounter & " needsvalue: " & $(ftype != "bool") & " default: " & $defValue & " multiple: " & $multiple) | |
218 | + let spec = PSpec(defVal:defValue, ptype: ftype, group: groupCounter, needsValue: ftype != "bool",multiple:multiple) | |
210 | 219 | aliases[alias] = name |
211 | 220 | parm_spec[name] = spec |
212 | 221 | |
... | ... | @@ -297,12 +306,19 @@ proc parseArguments*(usage: string, args: seq[string]): Table[string,PValue] = |
297 | 306 | if info.used: |
298 | 307 | if flag == "help" or flag == "version": |
299 | 308 | enableChecks = false |
300 | - | |
301 | - | |
309 | + | |
310 | + # Check maximum group used | |
311 | + var maxGroup = 0 | |
312 | + for item in flagvalues: | |
313 | + info = get_spec(item[0]) | |
314 | + if maxGroup < info.group: | |
315 | + maxGroup = info.group | |
316 | + | |
302 | 317 | # any flags not mentioned? |
303 | 318 | for flag,info in parm_spec: |
304 | 319 | if not info.used: |
305 | - if info.defVal == "": # no default! | |
320 | + # Is there no default and we have used options in this group? | |
321 | + if info.defVal == "" and info.group <= maxGroup: | |
306 | 322 | failures.add("required option or argument missing: " & flag) |
307 | 323 | else: |
308 | 324 | flagvalues.add(@[flag,info.defVal]) | ... | ... |