Created
January 6, 2017 01:42
-
-
Save codekaizen/811d8265a750edc14f29fd7d2c612269 to your computer and use it in GitHub Desktop.
Copy a branch from a Mercurial repository to a Git repository via patches and git commands
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* LinqPad header for .linq file | |
<Query Kind="Program"> | |
<NuGetReference>Newtonsoft.Json</NuGetReference> | |
<NuGetReference>QuickGraph</NuGetReference> | |
<NuGetReference>System.Reactive</NuGetReference> | |
<Namespace>Newtonsoft.Json</Namespace> | |
<Namespace>Newtonsoft.Json.Converters</Namespace> | |
<Namespace>Newtonsoft.Json.Linq</Namespace> | |
<Namespace>Newtonsoft.Json.Serialization</Namespace> | |
<Namespace>QuickGraph</Namespace> | |
<Namespace>System.Reactive</Namespace> | |
<Namespace>System.Reactive.Linq</Namespace> | |
<Namespace>System.Reactive.Subjects</Namespace> | |
<Namespace>System.Globalization</Namespace> | |
</Query> | |
*/ | |
/* | |
* Script to export branch changesets from original HG repository to target GIT repository. This script will generate the necessary git commands | |
* which need to be run by hand (currently). The reason for this is there are merge conflicts when merging master over that need to be handled. | |
* | |
* The script will generate commands to import the branch starting with the first source branch commit (should be a branch create). Execute each command in turn | |
* and the final command should have your branch recreated. | |
*/ | |
// If you want to ignore certain paths in the imported patch, they can be put here. | |
private static HashSet<String> _excludePaths = new HashSet<String> | |
{ | |
".hgsubstate", | |
".hgtags", | |
"prereq/*" | |
}; | |
void Main() | |
{ | |
// Put your source HG repository path here | |
var hgRepoPath = @"C:\HgPath"; | |
// Put your target GIT repository path here | |
var gitRepoPath = @"C:\NewGitPath"; | |
// Put the HG branch name you are wanting to export / import | |
var targetBranch = "My Branch Name"; | |
// Some path for HG to put the exported patches | |
var patchOutputDir = @"C:\temp\transplant"; | |
// Which GIT repository are you working on? | |
var targetRepo = "MyProject"; | |
var shouldRunExport = false; | |
// In the old repo, the files were grouped by subfolders, so we don't want to exclude the path which became the new Git repo. | |
_excludePaths.RemoveWhere(s => s.Contains(targetRepo)); | |
if (shouldRunExport) | |
{ | |
runHgBranchExport(hgRepoPath, targetBranch, patchOutputDir); | |
// The exported patch has paths that don't match the converted Git repo; this method fixes them up. Omit if the paths are the same. | |
processFilePathsInPatches(targetRepo, patchOutputDir); | |
} | |
var branchLog = getHgBranchLog(hgRepoPath, targetBranch); | |
var (hgGraph, nodeLookup) = createGraph(branchLog); | |
var parentsToLoad = nodeLookup.Values.Where(v => v.Entry == null).Select(v => v.Node).ToList(); | |
loadEntries(hgRepoPath, hgGraph, nodeLookup, parentsToLoad); | |
var defaultParents = branchLog | |
.SelectMany(l => l.parents.Select(p => (l, p))) | |
.Where(t => | |
{ | |
var (n, p) = t; | |
return !String.IsNullOrEmpty(p) && nodeLookup[p].Branch == "default"; | |
}) | |
.Select(t => | |
{ | |
var (n, p) = t; | |
var parent = nodeLookup[p]; | |
return new Merge | |
{ | |
TargetId = n.node, | |
DefaultParent = new NodeTag { Message = parent.Message, Date = parent.Entry.Date, User = parent.Entry.user }, | |
Target = new NodeTag { Message = n.desc, Date = n.Date, User = n.user } | |
}; | |
}).ToDictionary(k => k.TargetId); | |
var masterParents = findClosestGitChangesets(gitRepoPath, defaultParents.Values).ToDictionary(e => e.Item1.TargetId); | |
var output = from l in branchLog | |
let gitId = masterParents.ContainsKey(l.node) ? masterParents[l.node].Item2.Id : null | |
let o = new | |
{ | |
Id = l.node, | |
Message = l.desc, | |
Author = l.user, | |
Date = l.Date, | |
IsBranchCreate = (l.parents?.Length ?? 0) == 1 && defaultParents.ContainsKey(l.node), | |
Merge = defaultParents.ContainsKey(l.node) ? defaultParents[l.node] : null, | |
GitId = gitId, | |
IsMerge = !String.IsNullOrWhiteSpace(gitId) | |
} | |
select o; | |
//output.Dump(); | |
var commands = from o in output.Reverse() | |
let create = o.IsBranchCreate ? $"git checkout {o.GitId}\r\ngit branch features/{gitifyBranchName(targetBranch)}\r\ngit checkout features/{gitifyBranchName(targetBranch)}" : null | |
let apply = buildGitApply(patchOutputDir, o.Id) | |
let commit = $"git commit -m \"{o.Message}\" --author \"{o.Author}\" --date \"{o.Date}\"" | |
let merge = !o.IsBranchCreate && o.IsMerge ? $"git reset --hard HEAD\r\ngit merge --commit --no-ff -m \"Merge with master\" {o.GitId}" : null | |
select $"{create}\r\n{apply}\r\n{commit}\r\n{merge}"; | |
commands.Dump(); | |
} | |
private String gitifyBranchName(String hgBranchName) | |
{ | |
var gitBranch = Regex.Replace(hgBranchName, "[ .,\"/]", "_"); | |
return gitBranch; | |
} | |
private String buildGitApply(String patchOutputPath, String changesetId) | |
{ | |
if (patchOutputPath.Last() != '\\' && patchOutputPath.Last() != '/') | |
{ | |
patchOutputPath = patchOutputPath + "\\"; | |
} | |
var builder = | |
_excludePaths.Aggregate(new StringBuilder("git apply --index --cached --verbose "), (agg, e) => agg.Append($"--exclude=\"{e}\" ")); | |
builder.Append($"\"{patchOutputPath}{changesetId}.patch\""); | |
return builder.ToString(); | |
} | |
private void processFilePathsInPatches(String targetRepo, String patchDir) | |
{ | |
var patchFiles = Directory.GetFiles(patchDir); | |
var regex = new Regex($"/src/{targetRepo}/"); | |
foreach (var patch in patchFiles) | |
{ | |
String replaced; | |
using (var file = File.OpenText(patch)) | |
{ | |
var contents = file.ReadToEnd(); | |
replaced = regex.Replace(contents, "/src/"); | |
} | |
File.WriteAllText(patch, replaced); | |
} | |
} | |
private IEnumerable<(Merge, GitLogEntry)> findClosestGitChangesets(String gitRepoPath, IReadOnlyCollection<Merge> hgMerges) | |
{ | |
// git log --after="2016-10-05T00:00:00" --before="2016-10-05T23:59:59" --pretty="format:%H|%aI" | |
foreach (var merge in hgMerges) | |
{ | |
var mergeDate = merge.DefaultParent.Date; | |
var startDate = mergeDate.Date; | |
var endDate = mergeDate; | |
var isGitRefFound = false; | |
GitLogEntry entry = null; | |
do | |
{ | |
var gitEntries = getGitLog(gitRepoPath, $"--after=\"{startDate:yyyy-MM-ddTHH:mm:ssK}\" --before=\"{endDate:yyyy-MM-ddTHH:mm:ssK}\" --pretty=\"format:%H|%aI\""); | |
isGitRefFound = gitEntries.Any(); | |
if (isGitRefFound) | |
{ | |
entry = gitEntries.First(); | |
} | |
else | |
{ | |
startDate = startDate.AddDays(-1); | |
} | |
} while (!isGitRefFound); | |
yield return (merge, entry); | |
} | |
} | |
private void loadEntries(String repoPath, HgGraph graph, Dictionary<String, HgNode> nodeLookup, IEnumerable<String> entriesToLoad) | |
{ | |
var log = getHgEntriesLog(repoPath, entriesToLoad); | |
loadGraphFromLog(graph, nodeLookup, log); | |
} | |
private IReadOnlyCollection<HgLogEntry> readHgLog(TextReader reader) | |
{ | |
var serializer = JsonSerializer.Create(); | |
var logs = (HgLogEntry[])serializer.Deserialize(new JsonTextReader(reader), typeof(HgLogEntry[])); | |
return logs; | |
} | |
private IReadOnlyCollection<HgLogEntry> getHgEntriesLog(String repoPath, IEnumerable<String> entriesToLoad) | |
{ | |
var idCriteraBuilder = entriesToLoad.Aggregate(new StringBuilder(), (agg, e) => agg.Append($"id({e}) or ")); | |
idCriteraBuilder.Length -= 4; | |
return getHgLog(repoPath, $"--rev \"{idCriteraBuilder.ToString()}\""); | |
} | |
private IReadOnlyCollection<HgLogEntry> getHgBranchLog(String repoPath, String branchName) | |
{ | |
return getHgLog(repoPath, $"--branch \"{branchName}\""); | |
} | |
private IReadOnlyCollection<GitLogEntry> getGitLog(String repoPath, String logArguments) | |
{ | |
var procStart = new ProcessStartInfo | |
{ | |
FileName = "git.exe", | |
Arguments = $"log {logArguments}", | |
RedirectStandardOutput = true, | |
UseShellExecute = false, | |
WorkingDirectory = repoPath, | |
CreateNoWindow = true, | |
WindowStyle = ProcessWindowStyle.Hidden | |
}; | |
IEnumerable<String> readLines(StreamReader reader) | |
{ | |
String line; | |
while ((line = reader.ReadLine()) != null) | |
{ | |
yield return line; | |
} | |
} | |
using (var git = Process.Start(procStart)) | |
{ | |
var logLines = readLines(git.StandardOutput); | |
var entries = from l in logLines | |
let values = l.Split('|') | |
let entry = new GitLogEntry | |
{ | |
Id = values[0].Trim(), | |
Date = DateTimeOffset.ParseExact(values[1], "yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture) | |
} | |
select entry; | |
return entries.ToList(); | |
} | |
} | |
private void runHgBranchExport(String hgRepo, String branchName, String outputPath) | |
{ | |
// hg export --rev branch({targetBranch}) -o "C:\temp\AC\bt\%H.patch" | |
var procStart = new ProcessStartInfo | |
{ | |
FileName = "hg.exe", | |
Arguments = $"export --rev \"branch('{branchName}')\" -o \"{outputPath}\\%H.patch\"", | |
RedirectStandardOutput = true, | |
UseShellExecute = false, | |
WorkingDirectory = hgRepo, | |
CreateNoWindow = true, | |
WindowStyle = ProcessWindowStyle.Hidden | |
}; | |
using (var hg = new Process() { StartInfo = procStart }) | |
{ | |
hg.OutputDataReceived += (s, a) => { Console.WriteLine(a.Data); }; | |
hg.Start(); | |
hg.WaitForExit(); | |
} | |
} | |
private IReadOnlyCollection<HgLogEntry> getHgLog(String repoPath, String logArguments) | |
{ | |
var procStart = new ProcessStartInfo | |
{ | |
FileName = "hg.exe", | |
Arguments = $"log {logArguments} -Tjson", | |
RedirectStandardOutput = true, | |
UseShellExecute = false, | |
WorkingDirectory = repoPath, | |
CreateNoWindow = true, | |
WindowStyle = ProcessWindowStyle.Hidden | |
}; | |
using (var hg = Process.Start(procStart)) | |
{ | |
var jsonLog = hg.StandardOutput.ReadToEnd(); | |
return readHgLog(new StringReader(jsonLog)); | |
} | |
} | |
private (HgGraph, Dictionary<String, HgNode>) createGraph(IReadOnlyCollection<HgLogEntry> log) | |
{ | |
var graph = new HgGraph(log.Count, log.Count * 3); | |
var nodeLookup = new Dictionary<String, HgNode>(log.Count); | |
loadGraphFromLog(graph, nodeLookup, log); | |
return (graph, nodeLookup); | |
} | |
private void loadGraphFromLog(HgGraph graph, Dictionary<String, HgNode> nodeLookup, IReadOnlyCollection<HgLogEntry> log) | |
{ | |
foreach (var entry in log) | |
{ | |
HgNode node; | |
if (!nodeLookup.TryGetValue(entry.node, out node)) | |
{ | |
node = new HgNode(entry.node) | |
{ | |
Entry = entry | |
}; | |
nodeLookup[node.Node] = node; | |
graph.AddVertex(node); | |
} | |
else | |
{ | |
node.Entry = entry; | |
} | |
var parent1 = addParent(node.Parent1, graph, nodeLookup); | |
if (parent1 != null) | |
{ | |
graph.AddEdge(new Edge<HgNode>(parent1, node)); | |
} | |
var parent2 = addParent(node.Parent2, graph, nodeLookup); | |
if (parent2 != null) | |
{ | |
graph.AddEdge(new Edge<HgNode>(parent2, node)); | |
} | |
} | |
} | |
private HgNode addParent(String parentId, HgGraph graph, Dictionary<String, HgNode> nodeLookup) | |
{ | |
HgNode node = null; | |
if (!String.IsNullOrWhiteSpace(parentId)) | |
{ | |
if (!nodeLookup.TryGetValue(parentId, out node)) | |
{ | |
node = new HgNode(parentId); | |
nodeLookup[parentId] = node; | |
graph.AddVertex(node); | |
} | |
} | |
return node; | |
} | |
public class HgNode | |
{ | |
private HgLogEntry _entry; | |
public HgNode(String node) | |
{ | |
if (String.IsNullOrWhiteSpace(node)) | |
{ | |
throw new ArgumentException(nameof(node)); | |
} | |
Node = node; | |
} | |
public HgLogEntry Entry | |
{ | |
get { return _entry; } | |
set | |
{ | |
if (_entry != null) | |
{ | |
throw new InvalidOperationException("Entry already set"); | |
} | |
if (value != null && value.node != Node) | |
{ | |
throw new ArgumentException($"Incorrect entry: {value?.node} != {Node}"); | |
} | |
_entry = value; | |
} | |
} | |
public String Branch => Entry?.branch; | |
public String Node { get; } | |
public String Parent1 => (Entry?.parents?.Length ?? 0) > 0 ? Entry.parents[0] : null; | |
public String Parent2 => (Entry?.parents?.Length ?? 0) > 1 ? Entry.parents[1] : null; | |
public String Message => Entry?.desc; | |
} | |
public class HgLogEntry | |
{ | |
private static readonly DateTime _epoch = new DateTime(1970, 1, 1, 0, 0, 0, 0); | |
public Int32 rev { get; set; } | |
public String node { get; set; } | |
public String branch { get; set; } | |
public String desc { get; set; } | |
public String phase { get; set; } | |
public String user { get; set; } | |
public Int32[] date { get; set; } | |
public String[] bookmarks { get; set; } | |
public String[] tags { get; set; } | |
public String[] parents { get; set; } | |
[JsonIgnore] | |
public DateTimeOffset Date => new DateTimeOffset(_epoch.AddSeconds(date[0]), TimeSpan.FromSeconds(0)).ToOffset(TimeSpan.FromSeconds(-date[1])); | |
} | |
public class GitLogEntry | |
{ | |
public String Id { get; set; } | |
public DateTimeOffset Date { get; set; } | |
} | |
public class HgGraph : BidirectionalGraph<HgNode, Edge<HgNode>> | |
{ | |
public HgGraph() : base(false) { } | |
public HgGraph(Int32 vertexCapacity, Int32 edgeCapacity) : base(false, vertexCapacity, edgeCapacity) { } | |
} | |
public class Merge | |
{ | |
public String TargetId { get; set; } | |
public NodeTag Target { get; set; } | |
public NodeTag DefaultParent { get; set; } | |
} | |
public class NodeTag | |
{ | |
public String Message { get; set; } | |
public DateTimeOffset Date { get; set; } | |
public String User { get; set; } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment