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