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 # blimp is a little utility program for handling large files 3 # blimp is a little utility program for handling large files
4 # in git repositories. Its inspired by git-fat and s3annex 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,7 +11,7 @@ import md5, os, osproc, parseopt2, strutils, parsecfg, streams, lapp, subexes
11 # Use "blimp i mybigfile" to inflate it back to original size. 11 # Use "blimp i mybigfile" to inflate it back to original size.
12 # 12 #
13 # When deflated the file only has: 13 # When deflated the file only has:
14 -# "hash:" <filename> + <an md5sum> 14 +# "blimphash:" <filename> + <an md5sum>
15 # ...inside it. 15 # ...inside it.
16 # 16 #
17 # The file is copied over to a local dir: 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,14 +30,14 @@ import md5, os, osproc, parseopt2, strutils, parsecfg, streams, lapp, subexes
30 30
31 const 31 const
32 versionMajor* = 0 32 versionMajor* = 0
33 - versionMinor* = 2  
34 - versionPatch* = 1 33 + versionMinor* = 3
  34 + versionPatch* = 0
35 versionAsString* = $versionMajor & "." & $versionMinor & "." & $versionPatch 35 versionAsString* = $versionMajor & "." & $versionMinor & "." & $versionPatch
36 36
37 var 37 var
38 blimpStore, remoteBlimpStore, uploadCommandFormat, downloadCommandFormat, deleteCommandFormat, rsyncPassword, blimpVersion: string = nil 38 blimpStore, remoteBlimpStore, uploadCommandFormat, downloadCommandFormat, deleteCommandFormat, rsyncPassword, blimpVersion: string = nil
39 homeDir, currentDir, gitRootDir: string 39 homeDir, currentDir, gitRootDir: string
40 - verbose, stdio: bool 40 + verbose, stdio, inflateAll: bool
41 stdinContent: string = nil 41 stdinContent: string = nil
42 42
43 let 43 let
@@ -72,10 +72,11 @@ delete = &quot;rsync --password-file $3/.blimp.pass -dv --delete --existing --ignore- @@ -72,10 +72,11 @@ delete = &quot;rsync --password-file $3/.blimp.pass -dv --delete --existing --ignore-
72 # version = 0.2 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 try: 78 try:
78 - let tup = execCmdEx("git rev-parse --show-toplevel") 79 + let tup = execCmdEx(cmd)
79 if tup[1] == 0: 80 if tup[1] == 0:
80 result = strip(tup[0]) 81 result = strip(tup[0])
81 else: 82 else:
@@ -83,6 +84,29 @@ proc gitRoot(): string = @@ -83,6 +84,29 @@ proc gitRoot(): string =
83 except: 84 except:
84 result = nil 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 # Simple expansion of %home%, %cwd% and %gitroot% 110 # Simple expansion of %home%, %cwd% and %gitroot%
87 proc expandDirs(templ: string): string = 111 proc expandDirs(templ: string): string =
88 result = templ.replace("%home%", homeDir) 112 result = templ.replace("%home%", homeDir)
@@ -96,8 +120,7 @@ proc parseConfFile(filename: string) = @@ -96,8 +120,7 @@ proc parseConfFile(filename: string) =
96 var f = newFileStream(filename, fmRead) 120 var f = newFileStream(filename, fmRead)
97 if f != nil: 121 if f != nil:
98 if verbose: echo "Reading config: " & filename 122 if verbose: echo "Reading config: " & filename
99 - var p: CfgParser  
100 - 123 + var p: CfgParser
101 open(p, f, filename) 124 open(p, f, filename)
102 while true: 125 while true:
103 var e = next(p) 126 var e = next(p)
@@ -200,8 +223,8 @@ proc deleteFromBlimpStore(blimpFilename, filename: string) = @@ -200,8 +223,8 @@ proc deleteFromBlimpStore(blimpFilename, filename: string) =
200 remoteDeleteFile(blimpFilename) 223 remoteDeleteFile(blimpFilename)
201 224
202 proc blimpFileNameFromString(line: string): string = 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 result = hashline[1] 228 result = hashline[1]
206 else: 229 else:
207 result = nil 230 result = nil
@@ -236,11 +259,18 @@ proc deflate(filename: string) = @@ -236,11 +259,18 @@ proc deflate(filename: string) =
236 blimpFilename = computeBlimpFilename(filename) 259 blimpFilename = computeBlimpFilename(filename)
237 copyToBlimpStore(filename, blimpFilename) 260 copyToBlimpStore(filename, blimpFilename)
238 if stdio: 261 if stdio:
239 - write(stdout, "hash:" & blimpFilename) 262 + write(stdout, "blimphash:" & blimpFilename)
240 else: 263 else:
241 - writeFile(filename, "hash:" & blimpFilename) 264 + writeFile(filename, "blimphash:" & blimpFilename)
242 if verbose: echo("\t" & filename & " deflated.") 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 # Parse out hash from hash stub and copy back original content from blimpStore. 274 # Parse out hash from hash stub and copy back original content from blimpStore.
245 proc inflate(filename: string) = 275 proc inflate(filename: string) =
246 if verbose: echo "Inflating " & filename 276 if verbose: echo "Inflating " & filename
@@ -290,41 +320,45 @@ proc dumpConfig() = @@ -290,41 +320,45 @@ proc dumpConfig() =
290 echo "\tblimpVersion: " & $blimpVersion 320 echo "\tblimpVersion: " & $blimpVersion
291 echo "\n" 321 echo "\n"
292 322
293 -let help = """ 323 +let synopsis = """
294 blimp [options] <command> <filenames...> 324 blimp [options] <command> <filenames...>
295 -h,--help Show this 325 -h,--help Show this
296 --version Show version of blimp 326 --version Show version of blimp
297 -v,--verbose Verbosity, only works without -s 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 <command> (string) (d)eflate, (i)nflate, remove 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 blimp is a little utility program for handling large files 337 blimp is a little utility program for handling large files
303 in git repositories. Its inspired by git-fat and s3annex 338 in git repositories. Its inspired by git-fat and s3annex
304 but doesn't rely on S3 for storage - it uses rsync like git-fat. 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 Manual use: 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 When deflated the file only has: 348 When deflated the file only has:
313 - "hash:" <filename> "-" <md5sum> 349 + "blimphash:" <filename> "-" <md5sum>
314 ...inside it. 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 Configuration is in these locations in order: 358 Configuration is in these locations in order:
325 359
326 ./.blimp.conf 360 ./.blimp.conf
327 - "gitroot"/.blimp.conf 361 + <gitroot>/.blimp.conf
328 ~/<blimpstore>/.blimp.conf 362 ~/<blimpstore>/.blimp.conf
329 ~/.blimp.conf 363 ~/.blimp.conf
330 364
@@ -337,18 +371,40 @@ let help = &quot;&quot;&quot; @@ -337,18 +371,40 @@ let help = &quot;&quot;&quot;
337 synced with a master rsync repository that is typically shared. 371 synced with a master rsync repository that is typically shared.
338 372
339 Inflate will bring back the original content by copying from 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 Use this whenever you need to work/edit the big file - in order to get 375 Use this whenever you need to work/edit the big file - in order to get
342 its real content. 376 its real content.
343 377
344 The filenames given are all processed. If -s is used content is processed via 378 The filenames given are all processed. If -s is used content is processed via
345 stdin/stdout and only one filename can be passed. This is used when running blimp 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 The remove command (no single character shortcut) will remove the file(s) content 382 The remove command (no single character shortcut) will remove the file(s) content
349 both from the local blimpstore and from the remote. This only removes 383 both from the local blimpstore and from the remote. This only removes
350 the current content version, not older versions. The file itself is first 384 the current content version, not older versions. The file itself is first
351 inflated, if needed, and not deleted. This only "unblimps" the file. 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 ################################ main ##################################### 410 ################################ main #####################################
@@ -359,9 +415,10 @@ currentDir = getCurrentDir() @@ -359,9 +415,10 @@ currentDir = getCurrentDir()
359 gitRootDir = gitRoot() 415 gitRootDir = gitRoot()
360 416
361 # Using lapp to get args, on parsing failure this will show usage automatically 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 verbose = args["verbose"].asBool 419 verbose = args["verbose"].asBool
364 stdio = args["stdio"].asBool 420 stdio = args["stdio"].asBool
  421 +inflateAll = args["all"].asBool
365 422
366 # Can't do verbose with -s, that messes up stdout, 423 # Can't do verbose with -s, that messes up stdout,
367 # read in all of stdin once and for all 424 # read in all of stdin once and for all
@@ -376,7 +433,7 @@ if stdio: @@ -376,7 +433,7 @@ if stdio:
376 433
377 # Parse configuration files, may shadow and override each other 434 # Parse configuration files, may shadow and override each other
378 parseConfFile(currentDir / ".blimp.conf") 435 parseConfFile(currentDir / ".blimp.conf")
379 -if not gitRootDir.isNil: 436 +if not gitRootDir.isNil and gitRootDir != currentDir:
380 parseConfFile(gitRootDir / ".blimp.conf") 437 parseConfFile(gitRootDir / ".blimp.conf")
381 438
382 # If we haven't gotten a blimpstore yet, we set a default one 439 # If we haven't gotten a blimpstore yet, we set a default one
@@ -387,35 +444,47 @@ if existsDir(blimpStore): @@ -387,35 +444,47 @@ if existsDir(blimpStore):
387 parseConfFile(blimpStore / ".blimp.conf") 444 parseConfFile(blimpStore / ".blimp.conf")
388 parseConfFile(homeDir / ".blimp.conf") 445 parseConfFile(homeDir / ".blimp.conf")
389 446
  447 +# Let's just show what we have :)
390 if verbose: dumpConfig() 448 if verbose: dumpConfig()
391 449
392 # These two are special, they short out 450 # These two are special, they short out
393 -if args.showHelp: quit(help) 451 +if args.showHelp: quit(synopsis & help)
394 if args.showVersion: quit("blimp version: " & versionAsString) 452 if args.showVersion: quit("blimp version: " & versionAsString)
395 453
396 # Check blimpVersion 454 # Check blimpVersion
397 if not blimpVersion.isNil and blimpVersion != versionAsString: 455 if not blimpVersion.isNil and blimpVersion != versionAsString:
398 quit("Wrong version of blimp, configuration wants: " & blimpVersion) 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 let command = args["command"].asString 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 # Make sure the local blimpstore is setup. 468 # Make sure the local blimpstore is setup.
404 setupBlimpStore() 469 setupBlimpStore()
405 470
406 -  
407 # Do the deed 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 # All good 489 # All good
421 quit(0) 490 quit(0)
lapp.nim
@@ -17,6 +17,8 @@ type @@ -17,6 +17,8 @@ type
17 trange 17 trange
18 telipsis 18 telipsis
19 tchar 19 tchar
  20 + toption
  21 + tdivider
20 22
21 proc thisChar(L: PLexer):char = L.str[L.idx] 23 proc thisChar(L: PLexer):char = L.str[L.idx]
22 proc next(L: PLexer) = L.idx += 1 24 proc next(L: PLexer) = L.idx += 1
@@ -35,9 +37,15 @@ proc get(L: PLexer; t: var TLexType): string = @@ -35,9 +37,15 @@ proc get(L: PLexer; t: var TLexType): string =
35 t = tchar 37 t = tchar
36 case c 38 case c
37 of '-': # '-", "--" 39 of '-': # '-", "--"
  40 + t = toption
38 if thisChar(L) == '-': 41 if thisChar(L) == '-':
39 result.add('-') 42 result.add('-')
40 next(L) 43 next(L)
  44 + if thisChar(L) == '-': # "---..."
  45 + t = tdivider
  46 + result.add('-')
  47 + while thisChar(L) == '-':
  48 + next(L)
41 of Letters: # word 49 of Letters: # word
42 t = tword 50 t = tword
43 while thisChar(L) in Letters: 51 while thisChar(L) in Letters:
@@ -112,14 +120,13 @@ proc intValue(v: int): PValue = PValue(kind: vInt, asInt: v) @@ -112,14 +120,13 @@ proc intValue(v: int): PValue = PValue(kind: vInt, asInt: v)
112 proc floatValue(v: float): PValue = PValue(kind: vFloat, asFloat: v) 120 proc floatValue(v: float): PValue = PValue(kind: vFloat, asFloat: v)
113 121
114 proc seqValue(v: seq[PValue]): PValue = PValue(kind: vSeq, asSeq: v) 122 proc seqValue(v: seq[PValue]): PValue = PValue(kind: vSeq, asSeq: v)
115 -  
116 -const MAX_FILES = 30  
117 123
118 type 124 type
119 PSpec = ref TSpec 125 PSpec = ref TSpec
120 TSpec = object 126 TSpec = object
121 defVal: string 127 defVal: string
122 ptype: string 128 ptype: string
  129 + group: int
123 needsValue, multiple, used: bool 130 needsValue, multiple, used: bool
124 var 131 var
125 progname, usage: string 132 progname, usage: string
@@ -134,6 +141,7 @@ proc parseSpec(u: string) = @@ -134,6 +141,7 @@ proc parseSpec(u: string) =
134 var 141 var
135 L: PLexer 142 L: PLexer
136 tok: string 143 tok: string
  144 + groupCounter: int
137 k = 1 145 k = 1
138 146
139 let lines = u.splitLines 147 let lines = u.splitLines
@@ -173,7 +181,8 @@ proc parseSpec(u: string) = @@ -173,7 +181,8 @@ proc parseSpec(u: string) =
173 k += 1 181 k += 1
174 tok = L.get 182 tok = L.get
175 if tok != ">": fail("argument must be enclosed in <...>") 183 if tok != ">": fail("argument must be enclosed in <...>")
176 - 184 + elif tok == "---": # divider
  185 + inc(groupCounter)
177 if getnext: tok = L.get 186 if getnext: tok = L.get
178 if tok == ":": # allowed to have colon after flags 187 if tok == ":": # allowed to have colon after flags
179 tok = L.get 188 tok = L.get
@@ -205,8 +214,8 @@ proc parseSpec(u: string) = @@ -205,8 +214,8 @@ proc parseSpec(u: string) =
205 defValue = "false" 214 defValue = "false"
206 215
207 if name != nil: 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 aliases[alias] = name 219 aliases[alias] = name
211 parm_spec[name] = spec 220 parm_spec[name] = spec
212 221
@@ -297,12 +306,19 @@ proc parseArguments*(usage: string, args: seq[string]): Table[string,PValue] = @@ -297,12 +306,19 @@ proc parseArguments*(usage: string, args: seq[string]): Table[string,PValue] =
297 if info.used: 306 if info.used:
298 if flag == "help" or flag == "version": 307 if flag == "help" or flag == "version":
299 enableChecks = false 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 # any flags not mentioned? 317 # any flags not mentioned?
303 for flag,info in parm_spec: 318 for flag,info in parm_spec:
304 if not info.used: 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 failures.add("required option or argument missing: " & flag) 322 failures.add("required option or argument missing: " & flag)
307 else: 323 else:
308 flagvalues.add(@[flag,info.defVal]) 324 flagvalues.add(@[flag,info.defVal])