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