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