]> arthur.barton.de Git - bup.git/blob - cmd/midx-cmd.py
Only change LC_CTYPE for bup-python itself, not sh or subprocesses
[bup.git] / cmd / midx-cmd.py
1 #!/bin/sh
2 """": # -*-python-*-
3 bup_python="$(dirname "$0")/bup-python" || exit $?
4 exec "$bup_python" "$0" ${1+"$@"}
5 """
6 # end of bup preamble
7
8 from __future__ import absolute_import
9 import glob, math, os, resource, struct, sys, tempfile
10
11 from bup import options, git, midx, _helpers, xstat
12 from bup.compat import hexstr, range
13 from bup.helpers import (Sha1, add_error, atomically_replaced_file, debug1, fdatasync,
14                          handle_ctrl_c, log, mmap_readwrite, qprogress,
15                          saved_errors, unlink)
16
17
18 PAGE_SIZE=4096
19 SHA_PER_PAGE=PAGE_SIZE/20.
20
21 optspec = """
22 bup midx [options...] <idxnames...>
23 --
24 o,output=  output midx filename (default: auto-generated)
25 a,auto     automatically use all existing .midx/.idx files as input
26 f,force    merge produce exactly one .midx containing all objects
27 p,print    print names of generated midx files
28 check      validate contents of the given midx files (with -a, all midx files)
29 max-files= maximum number of idx files to open at once [-1]
30 d,dir=     directory containing idx/midx files
31 """
32
33 merge_into = _helpers.merge_into
34
35
36 def _group(l, count):
37     for i in range(0, len(l), count):
38         yield l[i:i+count]
39
40
41 def max_files():
42     mf = min(resource.getrlimit(resource.RLIMIT_NOFILE))
43     if mf > 32:
44         mf -= 20  # just a safety margin
45     else:
46         mf -= 6   # minimum safety margin
47     return mf
48
49
50 def check_midx(name):
51     nicename = git.repo_rel(name)
52     log('Checking %s.\n' % nicename)
53     try:
54         ix = git.open_idx(name)
55     except git.GitError as e:
56         add_error('%s: %s' % (name, e))
57         return
58     for count,subname in enumerate(ix.idxnames):
59         sub = git.open_idx(os.path.join(os.path.dirname(name), subname))
60         for ecount,e in enumerate(sub):
61             if not (ecount % 1234):
62                 qprogress('  %d/%d: %s %d/%d\r' 
63                           % (count, len(ix.idxnames),
64                              git.shorten_hash(subname), ecount, len(sub)))
65             if not sub.exists(e):
66                 add_error("%s: %s: %s missing from idx"
67                           % (nicename, git.shorten_hash(subname), hexstr(e)))
68             if not ix.exists(e):
69                 add_error("%s: %s: %s missing from midx"
70                           % (nicename, git.shorten_hash(subname), hexstr(e)))
71     prev = None
72     for ecount,e in enumerate(ix):
73         if not (ecount % 1234):
74             qprogress('  Ordering: %d/%d\r' % (ecount, len(ix)))
75         if not e >= prev:
76             add_error('%s: ordering error: %s < %s'
77                       % (nicename, hexstr(e), hexstr(prev)))
78         prev = e
79
80
81 _first = None
82 def _do_midx(outdir, outfilename, infilenames, prefixstr):
83     global _first
84     if not outfilename:
85         assert(outdir)
86         sum = Sha1('\0'.join(infilenames)).hexdigest()
87         outfilename = '%s/midx-%s.midx' % (outdir, sum)
88     
89     inp = []
90     total = 0
91     allfilenames = []
92     midxs = []
93     try:
94         for name in infilenames:
95             ix = git.open_idx(name)
96             midxs.append(ix)
97             inp.append((
98                 ix.map,
99                 len(ix),
100                 ix.sha_ofs,
101                 isinstance(ix, midx.PackMidx) and ix.which_ofs or 0,
102                 len(allfilenames),
103             ))
104             for n in ix.idxnames:
105                 allfilenames.append(os.path.basename(n))
106             total += len(ix)
107         inp.sort(reverse=True, key=lambda x: str(x[0][x[2]:x[2]+20]))
108
109         if not _first: _first = outdir
110         dirprefix = (_first != outdir) and git.repo_rel(outdir)+': ' or ''
111         debug1('midx: %s%screating from %d files (%d objects).\n'
112                % (dirprefix, prefixstr, len(infilenames), total))
113         if (opt.auto and (total < 1024 and len(infilenames) < 3)) \
114            or ((opt.auto or opt.force) and len(infilenames) < 2) \
115            or (opt.force and not total):
116             debug1('midx: nothing to do.\n')
117             return
118
119         pages = int(total/SHA_PER_PAGE) or 1
120         bits = int(math.ceil(math.log(pages, 2)))
121         entries = 2**bits
122         debug1('midx: table size: %d (%d bits)\n' % (entries*4, bits))
123
124         unlink(outfilename)
125         with atomically_replaced_file(outfilename, 'wb') as f:
126             f.write('MIDX')
127             f.write(struct.pack('!II', midx.MIDX_VERSION, bits))
128             assert(f.tell() == 12)
129
130             f.truncate(12 + 4*entries + 20*total + 4*total)
131             f.flush()
132             fdatasync(f.fileno())
133
134             fmap = mmap_readwrite(f, close=False)
135
136             count = merge_into(fmap, bits, total, inp)
137             del fmap # Assume this calls msync() now.
138             f.seek(0, os.SEEK_END)
139             f.write('\0'.join(allfilenames))
140     finally:
141         for ix in midxs:
142             if isinstance(ix, midx.PackMidx):
143                 ix.close()
144         midxs = None
145         inp = None
146
147
148     # This is just for testing (if you enable this, don't clear inp above)
149     if 0:
150         p = midx.PackMidx(outfilename)
151         assert(len(p.idxnames) == len(infilenames))
152         print p.idxnames
153         assert(len(p) == total)
154         for pe, e in p, git.idxmerge(inp, final_progress=False):
155             pin = next(pi)
156             assert(i == pin)
157             assert(p.exists(i))
158
159     return total, outfilename
160
161
162 def do_midx(outdir, outfilename, infilenames, prefixstr):
163     rv = _do_midx(outdir, outfilename, infilenames, prefixstr)
164     if rv and opt['print']:
165         print rv[1]
166
167
168 def do_midx_dir(path, outfilename):
169     already = {}
170     sizes = {}
171     if opt.force and not opt.auto:
172         midxs = []   # don't use existing midx files
173     else:
174         midxs = glob.glob('%s/*.midx' % path)
175         contents = {}
176         for mname in midxs:
177             m = git.open_idx(mname)
178             contents[mname] = [('%s/%s' % (path,i)) for i in m.idxnames]
179             sizes[mname] = len(m)
180                     
181         # sort the biggest+newest midxes first, so that we can eliminate
182         # smaller (or older) redundant ones that come later in the list
183         midxs.sort(key=lambda ix: (-sizes[ix], -xstat.stat(ix).st_mtime))
184         
185         for mname in midxs:
186             any = 0
187             for iname in contents[mname]:
188                 if not already.get(iname):
189                     already[iname] = 1
190                     any = 1
191             if not any:
192                 debug1('%r is redundant\n' % mname)
193                 unlink(mname)
194                 already[mname] = 1
195
196     midxs = [k for k in midxs if not already.get(k)]
197     idxs = [k for k in glob.glob('%s/*.idx' % path) if not already.get(k)]
198
199     for iname in idxs:
200         i = git.open_idx(iname)
201         sizes[iname] = len(i)
202
203     all = [(sizes[n],n) for n in (midxs + idxs)]
204     
205     # FIXME: what are the optimal values?  Does this make sense?
206     DESIRED_HWM = opt.force and 1 or 5
207     DESIRED_LWM = opt.force and 1 or 2
208     existed = dict((name,1) for sz,name in all)
209     debug1('midx: %d indexes; want no more than %d.\n' 
210            % (len(all), DESIRED_HWM))
211     if len(all) <= DESIRED_HWM:
212         debug1('midx: nothing to do.\n')
213     while len(all) > DESIRED_HWM:
214         all.sort()
215         part1 = [name for sz,name in all[:len(all)-DESIRED_LWM+1]]
216         part2 = all[len(all)-DESIRED_LWM+1:]
217         all = list(do_midx_group(path, outfilename, part1)) + part2
218         if len(all) > DESIRED_HWM:
219             debug1('\nStill too many indexes (%d > %d).  Merging again.\n'
220                    % (len(all), DESIRED_HWM))
221
222     if opt['print']:
223         for sz,name in all:
224             if not existed.get(name):
225                 print name
226
227
228 def do_midx_group(outdir, outfilename, infiles):
229     groups = list(_group(infiles, opt.max_files))
230     gprefix = ''
231     for n,sublist in enumerate(groups):
232         if len(groups) != 1:
233             gprefix = 'Group %d: ' % (n+1)
234         rv = _do_midx(outdir, outfilename, sublist, gprefix)
235         if rv:
236             yield rv
237
238
239 handle_ctrl_c()
240
241 o = options.Options(optspec)
242 (opt, flags, extra) = o.parse(sys.argv[1:])
243
244 if extra and (opt.auto or opt.force):
245     o.fatal("you can't use -f/-a and also provide filenames")
246 if opt.check and (not extra and not opt.auto):
247     o.fatal("if using --check, you must provide filenames or -a")
248
249 git.check_repo_or_die()
250
251 if opt.max_files < 0:
252     opt.max_files = max_files()
253 assert(opt.max_files >= 5)
254
255 if opt.check:
256     # check existing midx files
257     if extra:
258         midxes = extra
259     else:
260         midxes = []
261         paths = opt.dir and [opt.dir] or git.all_packdirs()
262         for path in paths:
263             debug1('midx: scanning %s\n' % path)
264             midxes += glob.glob(os.path.join(path, '*.midx'))
265     for name in midxes:
266         check_midx(name)
267     if not saved_errors:
268         log('All tests passed.\n')
269 else:
270     if extra:
271         do_midx(git.repo('objects/pack'), opt.output, extra, '')
272     elif opt.auto or opt.force:
273         paths = opt.dir and [opt.dir] or git.all_packdirs()
274         for path in paths:
275             debug1('midx: scanning %s\n' % path)
276             do_midx_dir(path, opt.output)
277     else:
278         o.fatal("you must use -f or -a or provide input filenames")
279
280 if saved_errors:
281     log('WARNING: %d errors encountered.\n' % len(saved_errors))
282     sys.exit(1)