1#! /usr/bin/env python
2
3"""Script to synchronize two source trees.
4
5Invoke with two arguments:
6
7python treesync.py slave master
8
9The assumption is that "master" contains CVS administration while
10slave doesn't.  All files in the slave tree that have a CVS/Entries
11entry in the master tree are synchronized.  This means:
12
13    If the files differ:
14        if the slave file is newer:
15            normalize the slave file
16            if the files still differ:
17                copy the slave to the master
18        else (the master is newer):
19            copy the master to the slave
20
21    normalizing the slave means replacing CRLF with LF when the master
22    doesn't use CRLF
23
24"""
25
26import os, sys, stat, getopt
27
28# Interactivity options
29default_answer = "ask"
30create_files = "yes"
31create_directories = "no"
32write_slave = "ask"
33write_master = "ask"
34
35def main():
36    global always_no, always_yes
37    global create_directories, write_master, write_slave
38    opts, args = getopt.getopt(sys.argv[1:], "nym:s:d:f:a:")
39    for o, a in opts:
40        if o == '-y':
41            default_answer = "yes"
42        if o == '-n':
43            default_answer = "no"
44        if o == '-s':
45            write_slave = a
46        if o == '-m':
47            write_master = a
48        if o == '-d':
49            create_directories = a
50        if o == '-f':
51            create_files = a
52        if o == '-a':
53            create_files = create_directories = write_slave = write_master = a
54    try:
55        [slave, master] = args
56    except ValueError:
57        print "usage: python", sys.argv[0] or "treesync.py",
58        print "[-n] [-y] [-m y|n|a] [-s y|n|a] [-d y|n|a] [-f n|y|a]",
59        print "slavedir masterdir"
60        return
61    process(slave, master)
62
63def process(slave, master):
64    cvsdir = os.path.join(master, "CVS")
65    if not os.path.isdir(cvsdir):
66        print "skipping master subdirectory", master
67        print "-- not under CVS"
68        return
69    print "-"*40
70    print "slave ", slave
71    print "master", master
72    if not os.path.isdir(slave):
73        if not okay("create slave directory %s?" % slave,
74                    answer=create_directories):
75            print "skipping master subdirectory", master
76            print "-- no corresponding slave", slave
77            return
78        print "creating slave directory", slave
79        try:
80            os.mkdir(slave)
81        except os.error, msg:
82            print "can't make slave directory", slave, ":", msg
83            return
84        else:
85            print "made slave directory", slave
86    cvsdir = None
87    subdirs = []
88    names = os.listdir(master)
89    for name in names:
90        mastername = os.path.join(master, name)
91        slavename = os.path.join(slave, name)
92        if name == "CVS":
93            cvsdir = mastername
94        else:
95            if os.path.isdir(mastername) and not os.path.islink(mastername):
96                subdirs.append((slavename, mastername))
97    if cvsdir:
98        entries = os.path.join(cvsdir, "Entries")
99        for e in open(entries).readlines():
100            words = e.split('/')
101            if words[0] == '' and words[1:]:
102                name = words[1]
103                s = os.path.join(slave, name)
104                m = os.path.join(master, name)
105                compare(s, m)
106    for (s, m) in subdirs:
107        process(s, m)
108
109def compare(slave, master):
110    try:
111        sf = open(slave, 'r')
112    except IOError:
113        sf = None
114    try:
115        mf = open(master, 'rb')
116    except IOError:
117        mf = None
118    if not sf:
119        if not mf:
120            print "Neither master nor slave exists", master
121            return
122        print "Creating missing slave", slave
123        copy(master, slave, answer=create_files)
124        return
125    if not mf:
126        print "Not updating missing master", master
127        return
128    if sf and mf:
129        if identical(sf, mf):
130            return
131    sft = mtime(sf)
132    mft = mtime(mf)
133    if mft > sft:
134        # Master is newer -- copy master to slave
135        sf.close()
136        mf.close()
137        print "Master             ", master
138        print "is newer than slave", slave
139        copy(master, slave, answer=write_slave)
140        return
141    # Slave is newer -- copy slave to master
142    print "Slave is", sft-mft, "seconds newer than master"
143    # But first check what to do about CRLF
144    mf.seek(0)
145    fun = funnychars(mf)
146    mf.close()
147    sf.close()
148    if fun:
149        print "***UPDATING MASTER (BINARY COPY)***"
150        copy(slave, master, "rb", answer=write_master)
151    else:
152        print "***UPDATING MASTER***"
153        copy(slave, master, "r", answer=write_master)
154
155BUFSIZE = 16*1024
156
157def identical(sf, mf):
158    while 1:
159        sd = sf.read(BUFSIZE)
160        md = mf.read(BUFSIZE)
161        if sd != md: return 0
162        if not sd: break
163    return 1
164
165def mtime(f):
166    st = os.fstat(f.fileno())
167    return st[stat.ST_MTIME]
168
169def funnychars(f):
170    while 1:
171        buf = f.read(BUFSIZE)
172        if not buf: break
173        if '\r' in buf or '\0' in buf: return 1
174    return 0
175
176def copy(src, dst, rmode="rb", wmode="wb", answer='ask'):
177    print "copying", src
178    print "     to", dst
179    if not okay("okay to copy? ", answer):
180        return
181    f = open(src, rmode)
182    g = open(dst, wmode)
183    while 1:
184        buf = f.read(BUFSIZE)
185        if not buf: break
186        g.write(buf)
187    f.close()
188    g.close()
189
190def okay(prompt, answer='ask'):
191    answer = answer.strip().lower()
192    if not answer or answer[0] not in 'ny':
193        answer = raw_input(prompt)
194        answer = answer.strip().lower()
195        if not answer:
196            answer = default_answer
197    if answer[:1] == 'y':
198        return 1
199    if answer[:1] == 'n':
200        return 0
201    print "Yes or No please -- try again:"
202    return okay(prompt)
203
204if __name__ == '__main__':
205    main()
206