1#! /usr/bin/env python3
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", end=' ')
58        print("[-n] [-y] [-m y|n|a] [-s y|n|a] [-d y|n|a] [-f n|y|a]", end=' ')
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 OSError as 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 raw_input(prompt):
191    sys.stdout.write(prompt)
192    sys.stdout.flush()
193    return sys.stdin.readline()
194
195def okay(prompt, answer='ask'):
196    answer = answer.strip().lower()
197    if not answer or answer[0] not in 'ny':
198        answer = input(prompt)
199        answer = answer.strip().lower()
200        if not answer:
201            answer = default_answer
202    if answer[:1] == 'y':
203        return 1
204    if answer[:1] == 'n':
205        return 0
206    print("Yes or No please -- try again:")
207    return okay(prompt)
208
209if __name__ == '__main__':
210    main()
211