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