Skip to content

Instantly share code, notes, and snippets.

@codekaizen
Created January 6, 2017 01:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save codekaizen/811d8265a750edc14f29fd7d2c612269 to your computer and use it in GitHub Desktop.
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
/* 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