Mercurial > hg-git-sync
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) |