comparison hggit_sync.py @ 2:19156ccdc3e1

Handle multiple commits at the same timestamp. Yes, that happens.
author Ludovic Chabant <ludovic@chabant.com>
date Tue, 05 Apr 2016 22:10:29 -0700
parents e9f44d2deb94
children 7d99080f276f
comparison
equal deleted inserted replaced
1:e9f44d2deb94 2:19156ccdc3e1
35 35
36 36
37 def build_commit_map(commits1, commits2): 37 def build_commit_map(commits1, commits2):
38 commits1 = sorted(commits1, key=lambda c: c.timestamp) 38 commits1 = sorted(commits1, key=lambda c: c.timestamp)
39 commits2 = sorted(commits2, key=lambda c: c.timestamp) 39 commits2 = sorted(commits2, key=lambda c: c.timestamp)
40 commit_map = dict(map(lambda c: (c.timestamp, (c, None)), commits1)) 40
41 # Build the commit map with the "left" commits' info.
42 commit_map = {}
43 for c1 in commits1:
44 if c1.timestamp not in commit_map:
45 commit_map[c1.timestamp] = [(c1, None)]
46 else:
47 # Each entry in the map is a list in case we have commits at the
48 # same timestamp. It's rare, but it happens, especially with
49 # people who have some fucked-up Git-fu.
50 print("Conflicting timestamps between %s and %s" %
51 (c1.nodeid, commit_map[c1.timestamp][0][0].nodeid))
52 commit_map[c1.timestamp].append((c1, None))
53
54 # Now put the "easy" matches from the "right" commits.
41 orphan_commits2 = [] 55 orphan_commits2 = []
42 print("Building commit map...") 56 print("Building commit map...")
43 for c in commits2: 57 for c in commits2:
44 entry = commit_map.get(c.timestamp, (None, None)) 58 entry = commit_map.get(c.timestamp)
45 entry = (entry[0], c) 59 if entry is None:
46 commit_map[c.timestamp] = entry 60 # No "left" commit had this timestamp... we'll have an orphan.
47 if entry[0] is None: 61 entry = [(None, None)]
62 commit_map[c.timestamp] = entry
63
64 # Add the commit info.
65 idx = 0
66 if len(entry) > 1:
67 for i, e in enumerate(entry):
68 if e[0] and e[0].description.strip() == c.description.strip():
69 idx = i
70 break
71 if entry[idx][1] is None:
72 entry[idx] = (entry[idx][0], c)
73 else:
74 print("Attempting to match 2 commits (%s and %s) to the same base "
75 "commit %s... creating orphan instead." %
76 (entry[idx][1].nodeid, c.nodeid, entry[idx][0].nodeid))
77 entry.append((None, c))
78 idx = len(entry) - 1
79 if entry[idx][0] is None:
48 orphan_commits2.append(c) 80 orphan_commits2.append(c)
49 81
50 if orphan_commits2: 82 orphan_commits1 = []
83 for entry in commit_map.values():
84 for e in entry:
85 if e[1] is None:
86 orphan_commits1.append(e[0])
87
88 if orphan_commits1 or orphan_commits2:
51 print("Fixing orphaned commits...") 89 print("Fixing orphaned commits...")
52 did_fix = True 90 did_fix = True
53 orphan_commits1 = [e[0] for e in commit_map.values() if e[1] is None]
54 while did_fix: 91 while did_fix:
55 did_fix = False 92 did_fix = False
56 for c2 in orphan_commits2: 93 for c2 in orphan_commits2:
57 for c1 in orphan_commits1: 94 for c1 in orphan_commits1:
58 if c1.description.strip() == c2.description.strip(): 95 if c1.description.strip() == c2.description.strip():
59 print("Mapping '%s' to '%s'" % (c1.nodeid, c2.nodeid)) 96 print("Mapping '%s' to '%s'" % (c1.nodeid, c2.nodeid))
60 print(" Similar description: %s" % c1.description) 97 print(" Same description: %s" % c1.description)
61 print(" Timestamp difference: %d" % (c2.timestamp - c1.timestamp)) 98 print(" Timestamp difference: %d" % (c2.timestamp - c1.timestamp))
62 commit_map[c1.timestamp] = (c1, c2) 99 entry = commit_map[c1.timestamp]
100 for i, e in enumerate(entry):
101 if e[0] and e[0].nodeid == c1.nodeid:
102 entry[i] = (c1, c2)
103 break
104 entry = commit_map[c2.timestamp]
105 for i, e in enumerate(entry):
106 if e[1] and e[1].nodeid == c2.nodeid:
107 assert e[0] is None
108 del entry[i]
109 if len(entry) == 0:
110 del commit_map[c2.timestamp]
63 orphan_commits1.remove(c1) 111 orphan_commits1.remove(c1)
64 orphan_commits2.remove(c2) 112 orphan_commits2.remove(c2)
65 did_fix = True 113 did_fix = True
66 break 114 break
67 if did_fix: 115 if did_fix:
68 break 116 break
117
118 if orphan_commits1 or orphan_commits2:
119 print("Still have %d and %d orphaned commits." %
120 (len(orphan_commits1), len(orphan_commits2)))
69 121
70 return commit_map 122 return commit_map
71 123
72 124
73 def main(): 125 def main():
92 return 1 144 return 1
93 145
94 git_repo = os.path.join(hg_repo, '.hg', 'git') 146 git_repo = os.path.join(hg_repo, '.hg', 'git')
95 if res.rebuild: 147 if res.rebuild:
96 print("Removing existing Git repo...") 148 print("Removing existing Git repo...")
97 shutil.rmtree(git_repo) 149 if os.path.isdir(git_repo):
150 shutil.rmtree(git_repo)
98 print("Syncing it again into: %s" % git_repo) 151 print("Syncing it again into: %s" % git_repo)
99 git_output = subprocess.check_output([ 152 git_output = subprocess.check_output([
100 'git', 'clone', '--bare', res.rebuild, git_repo]) 153 'git', 'clone', '--bare', res.rebuild, git_repo])
101 154
102 if not os.path.exists(git_repo): 155 if not os.path.exists(git_repo):
114 'git', 'log', '--format=%H %ct%n%s%n%n']) 167 'git', 'log', '--format=%H %ct%n%s%n%n'])
115 git_commits = parse_commits(git_output) 168 git_commits = parse_commits(git_output)
116 os.chdir(hg_repo) 169 os.chdir(hg_repo)
117 170
118 commit_map = build_commit_map(git_commits, hg_commits) 171 commit_map = build_commit_map(git_commits, hg_commits)
119 for key, val in commit_map.iteritems(): 172 for key, vals in commit_map.iteritems():
120 if val[0] is None: 173 for val in vals:
121 print("Mercurial commit '%s' (%s) has no Git mirror yet: %s" % 174 if val[0] is None:
122 (val[1].nodeid, val[1].timestamp, val[1].description)) 175 print("Mercurial commit '%s' (%s) has no Git mirror yet: %s" %
123 if val[1] is None: 176 (val[1].nodeid, val[1].timestamp, val[1].description))
124 print("Git commit '%s' (%s) is new: %s" % 177 if val[1] is None:
125 (val[0].nodeid, val[0].timestamp, val[0].description)) 178 print("Git commit '%s' (%s) is new: %s" %
179 (val[0].nodeid, val[0].timestamp, val[0].description))
126 180
127 map_file = res.mapfile or os.path.join(hg_repo, '.hg', 'git-mapfile') 181 map_file = res.mapfile or os.path.join(hg_repo, '.hg', 'git-mapfile')
128 print("Saving map file: %s" % map_file) 182 print("Saving map file: %s" % map_file)
129 with codecs.open(map_file, 'w', encoding='utf8') as fp: 183 with codecs.open(map_file, 'w', encoding='utf8') as fp:
130 for key, val in commit_map.iteritems(): 184 for key, vals in commit_map.iteritems():
131 if val[0] is None or val[1] is None: 185 for val in vals:
132 continue 186 if val[0] is None or val[1] is None:
133 fp.write(val[0].nodeid) 187 continue
134 fp.write(' ') 188 fp.write(val[0].nodeid)
135 fp.write(val[1].nodeid) 189 fp.write(' ')
136 fp.write('\n') 190 fp.write(val[1].nodeid)
191 fp.write('\n')
137 192
138 193
139 if __name__ == '__main__': 194 if __name__ == '__main__':
140 res = main() 195 res = main()
141 sys.exit(res) 196 sys.exit(res)