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]) | ... | ... |