]> arthur.barton.de Git - bup.git/blob - cmd-index.py
47302c615ec5be72da134afff19e8a7ce0c5e34a
[bup.git] / cmd-index.py
1 #!/usr/bin/env python2.5
2 import os, sys, stat
3 import options, git, index
4 from helpers import *
5
6
7 try:
8     O_LARGEFILE = os.O_LARGEFILE
9 except AttributeError:
10     O_LARGEFILE = 0
11
12
13 class OsFile:
14     def __init__(self, path):
15         self.fd = None
16         self.fd = os.open(path, os.O_RDONLY|O_LARGEFILE|os.O_NOFOLLOW)
17         
18     def __del__(self):
19         if self.fd:
20             fd = self.fd
21             self.fd = None
22             os.close(fd)
23
24     def fchdir(self):
25         os.fchdir(self.fd)
26
27
28 saved_errors = []
29 def add_error(e):
30     saved_errors.append(e)
31     log('\n%s\n' % e)
32
33
34 # the use of fchdir() and lstat() are for two reasons:
35 #  - help out the kernel by not making it repeatedly look up the absolute path
36 #  - avoid race conditions caused by doing listdir() on a changing symlink
37 def handle_path(ri, wi, dir, name, pst, xdev, can_delete_siblings):
38     hashgen = None
39     if opt.fake_valid:
40         def hashgen(name):
41             return (0, index.FAKE_SHA)
42     
43     dirty = 0
44     path = dir + name
45     #log('handle_path(%r,%r)\n' % (dir, name))
46     if stat.S_ISDIR(pst.st_mode):
47         if opt.verbose == 1: # log dirs only
48             sys.stdout.write('%s\n' % path)
49             sys.stdout.flush()
50         try:
51             OsFile(name).fchdir()
52         except OSError, e:
53             add_error(Exception('in %s: %s' % (dir, str(e))))
54             return 0
55         try:
56             try:
57                 ld = os.listdir('.')
58                 #log('* %r: %r\n' % (name, ld))
59             except OSError, e:
60                 add_error(Exception('in %s: %s' % (path, str(e))))
61                 return 0
62             lds = []
63             for p in ld:
64                 try:
65                     st = os.lstat(p)
66                 except OSError, e:
67                     add_error(Exception('in %s: %s' % (path, str(e))))
68                     continue
69                 if xdev != None and st.st_dev != xdev:
70                     log('Skipping %r: different filesystem.\n' 
71                         % index.realpath(p))
72                     continue
73                 if stat.S_ISDIR(st.st_mode):
74                     p = slashappend(p)
75                 lds.append((p, st))
76             for p,st in reversed(sorted(lds)):
77                 dirty += handle_path(ri, wi, path, p, st, xdev,
78                                      can_delete_siblings = True)
79         finally:
80             os.chdir('..')
81     #log('endloop: ri.cur:%r path:%r\n' % (ri.cur.name, path))
82     while ri.cur and ri.cur.name > path:
83         #log('ricur:%r path:%r\n' % (ri.cur, path))
84         if can_delete_siblings and dir and ri.cur.name.startswith(dir):
85             #log('    --- deleting\n')
86             ri.cur.flags &= ~(index.IX_EXISTS | index.IX_HASHVALID)
87             ri.cur.repack()
88             dirty += 1
89         ri.next()
90     if ri.cur and ri.cur.name == path:
91         dirty += ri.cur.from_stat(pst)
92         if dirty or not (ri.cur.flags & index.IX_HASHVALID):
93             #log('   --- updating %r\n' % path)
94             if hashgen:
95                 (ri.cur.gitmode, ri.cur.sha) = hashgen(name)
96                 ri.cur.flags |= index.IX_HASHVALID
97             ri.cur.repack()
98         ri.next()
99     else:
100         wi.add(path, pst, hashgen = hashgen)
101         dirty += 1
102     if opt.verbose > 1:  # all files, not just dirs
103         sys.stdout.write('%s\n' % path)
104         sys.stdout.flush()
105     return dirty
106
107
108 def merge_indexes(out, r1, r2):
109     log('bup: merging indexes.\n')
110     for e in index._last_writer_wins_iter([r1, r2]):
111         #if e.flags & index.IX_EXISTS:
112             out.add_ixentry(e)
113
114
115 class MergeGetter:
116     def __init__(self, l):
117         self.i = iter(l)
118         self.cur = None
119         self.next()
120
121     def next(self):
122         try:
123             self.cur = self.i.next()
124         except StopIteration:
125             self.cur = None
126         return self.cur
127
128
129 def update_index(path):
130     ri = index.Reader(indexfile)
131     wi = index.Writer(indexfile)
132     rig = MergeGetter(ri)
133     
134     rpath = index.realpath(path)
135     st = os.lstat(rpath)
136     if opt.xdev:
137         xdev = st.st_dev
138     else:
139         xdev = None
140     f = OsFile('.')
141     if rpath[-1] == '/':
142         rpath = rpath[:-1]
143     (dir, name) = os.path.split(rpath)
144     dir = slashappend(dir)
145     if stat.S_ISDIR(st.st_mode) and (not rpath or rpath[-1] != '/'):
146         name += '/'
147         can_delete_siblings = True
148     else:
149         can_delete_siblings = False
150     OsFile(dir or '/').fchdir()
151     dirty = handle_path(rig, wi, dir, name, st, xdev, can_delete_siblings)
152
153     # make sure all the parents of the updated path exist and are invalidated
154     # if appropriate.
155     while 1:
156         (rpath, junk) = os.path.split(rpath)
157         if not rpath:
158             break
159         elif rpath == '/':
160             p = rpath
161         else:
162             p = rpath + '/'
163         while rig.cur and rig.cur.name > p:
164             #log('FINISHING: %r path=%r d=%r\n' % (rig.cur.name, p, dirty))
165             rig.next()
166         if rig.cur and rig.cur.name == p:
167             if dirty:
168                 rig.cur.flags &= ~index.IX_HASHVALID
169                 rig.cur.repack()
170         else:
171             wi.add(p, os.lstat(p))
172         if p == '/':
173             break
174     
175     f.fchdir()
176     ri.save()
177     if wi.count:
178         mi = index.Writer(indexfile)
179         merge_indexes(mi, ri, wi.new_reader())
180         ri.close()
181         mi.close()
182     wi.abort()
183
184
185 optspec = """
186 bup index <-p|s|m|u> [options...] <filenames...>
187 --
188 p,print    print the index entries for the given names (also works with -u)
189 m,modified print only added/deleted/modified files (implies -p)
190 s,status   print each filename with a status char (A/M/D) (implies -p)
191 H,hash     print the hash for each object next to its name (implies -p)
192 u,update   (recursively) update the index entries for the given filenames
193 x,xdev,one-file-system  don't cross filesystem boundaries
194 fake-valid    mark all index entries as up-to-date even if they aren't
195 f,indexfile=  the name of the index file (default 'index')
196 v,verbose  increase log output (can be used more than once)
197 """
198 o = options.Options('bup index', optspec)
199 (opt, flags, extra) = o.parse(sys.argv[1:])
200
201 if not (opt.modified or opt['print'] or opt.status or opt.update):
202     log('bup index: you must supply one or more of -p, -s, -m, or -u\n')
203     o.usage()
204 if opt.fake_valid and not opt.update:
205     log('bup index: --fake-valid is meaningless without -u\n')
206     o.usage()
207
208 git.check_repo_or_die()
209 indexfile = opt.indexfile or git.repo('bupindex')
210
211 paths = index.reduce_paths(extra)
212
213 if opt.update:
214     if not paths:
215         log('bup index: update (-u) requested but no paths given\n')
216         o.usage()
217     for (rp, path) in paths:
218         update_index(rp)
219
220 if opt['print'] or opt.status or opt.modified:
221     for (name, ent) in index.Reader(indexfile).filter(extra or ['']):
222         if opt.modified and (ent.flags & index.IX_HASHVALID
223                              or stat.S_ISDIR(ent.mode)):
224             continue
225         line = ''
226         if opt.status:
227             if not ent.flags & index.IX_EXISTS:
228                 line += 'D '
229             elif not ent.flags & index.IX_HASHVALID:
230                 if ent.sha == index.EMPTY_SHA:
231                     line += 'A '
232                 else:
233                     line += 'M '
234             else:
235                 line += '  '
236         if opt.hash:
237             line += ent.sha.encode('hex') + ' '
238         print line + (name or './')
239         #print repr(ent)
240
241 if saved_errors:
242     log('WARNING: %d errors encountered.\n' % len(saved_errors))
243     exit(1)