Commit 386557e646a97464c713aa6ee607008c85f37aad

Authored by Göran Krampe
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 = &quot;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 = &quot;&quot;&quot;
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])
... ...