PageRenderTime 55ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/Kudu.Core/SourceControl/Git/GitExeRepository.cs

https://github.com/moacap/kudu
C# | 780 lines | 639 code | 109 blank | 32 comment | 72 complexity | 73737d31c647a3888ca523db9badcefb MD5 | raw file
Possible License(s): Apache-2.0
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Text;
  7. using Kudu.Contracts.Tracing;
  8. using Kudu.Core.Infrastructure;
  9. using Kudu.Core.Tracing;
  10. namespace Kudu.Core.SourceControl.Git
  11. {
  12. /// <summary>
  13. /// Implementation of a git repository over git.exe
  14. /// </summary>
  15. public class GitExeRepository : IRepository
  16. {
  17. private readonly GitExecutable _gitExe;
  18. private readonly ITraceFactory _tracerFactory;
  19. public GitExeRepository(string path)
  20. : this(path, NullTracerFactory.Instance)
  21. {
  22. }
  23. public GitExeRepository(string path, ITraceFactory profilerFactory)
  24. {
  25. _gitExe = new GitExecutable(path);
  26. _tracerFactory = profilerFactory;
  27. }
  28. public string CurrentId
  29. {
  30. get
  31. {
  32. return Resolve("HEAD");
  33. }
  34. }
  35. public void Initialize()
  36. {
  37. var profiler = _tracerFactory.GetTracer();
  38. using (profiler.Step("GitExeRepository.Initialize"))
  39. {
  40. _gitExe.Execute(profiler, "init");
  41. _gitExe.Execute(profiler, "config core.autocrlf true");
  42. }
  43. }
  44. public string Resolve(string id)
  45. {
  46. return _gitExe.Execute("rev-parse {0}", id).Item1.Trim();
  47. }
  48. public IEnumerable<FileStatus> GetStatus()
  49. {
  50. string status = _gitExe.Execute("status --porcelain").Item1;
  51. return ParseStatus(status.AsReader());
  52. }
  53. public IEnumerable<ChangeSet> GetChanges()
  54. {
  55. return Log();
  56. }
  57. public ChangeSet GetChangeSet(string id)
  58. {
  59. string showCommit = _gitExe.Execute("show {0} -m --name-status", id).Item1;
  60. var commitReader = showCommit.AsReader();
  61. return ParseCommit(commitReader);
  62. }
  63. public IEnumerable<ChangeSet> GetChanges(int index, int limit)
  64. {
  65. return Log("log --all --skip {0} -n {1}", index, limit);
  66. }
  67. public void AddFile(string path)
  68. {
  69. _gitExe.Execute("add {0}", path);
  70. }
  71. public void RevertFile(string path)
  72. {
  73. if (IsEmpty())
  74. {
  75. _gitExe.Execute("rm --cached \"{0}\"", path);
  76. }
  77. else
  78. {
  79. try
  80. {
  81. _gitExe.Execute("reset HEAD \"{0}\"", path);
  82. }
  83. catch
  84. {
  85. // This command returns a non zero exit code even when it succeeds
  86. }
  87. }
  88. // Now get the status of the file
  89. var fileStatuses = GetStatus().ToDictionary(fs => fs.Path, fs => fs.Status);
  90. ChangeType status;
  91. if (fileStatuses.TryGetValue(path, out status) && status != ChangeType.Untracked)
  92. {
  93. _gitExe.Execute("checkout -- \"{0}\"", path);
  94. }
  95. else
  96. {
  97. // If the file is untracked, delete it
  98. string fullPath = Path.Combine(_gitExe.WorkingDirectory, path);
  99. File.Delete(fullPath);
  100. }
  101. }
  102. public ChangeSet Commit(string message, string authorName = null)
  103. {
  104. ITracer tracer = _tracerFactory.GetTracer();
  105. // Add all unstaged files
  106. _gitExe.Execute(tracer, "add -A");
  107. try
  108. {
  109. string output;
  110. if (authorName == null)
  111. {
  112. output = _gitExe.Execute(tracer, "commit -m\"{0}\"", message).Item1;
  113. }
  114. else
  115. {
  116. output = _gitExe.Execute("commit -m\"{0}\" --author=\"{1}\"", message, authorName).Item1;
  117. }
  118. // No pending changes
  119. if (output.Contains("working directory clean"))
  120. {
  121. return null;
  122. }
  123. string newCommit = _gitExe.Execute(tracer, "show HEAD").Item1;
  124. using (tracer.Step("Parsing commit information"))
  125. {
  126. return ParseCommit(newCommit.AsReader());
  127. }
  128. }
  129. catch (Exception e)
  130. {
  131. if (e.Message.Contains("nothing to commit"))
  132. {
  133. return null;
  134. }
  135. throw;
  136. }
  137. }
  138. internal void Clone(string source)
  139. {
  140. _gitExe.Execute(@"clone ""{0}"" .", source);
  141. }
  142. public void Clean()
  143. {
  144. _gitExe.Execute(@"clean -xdf");
  145. }
  146. public void Push()
  147. {
  148. _gitExe.Execute(@"push origin master");
  149. }
  150. public void FetchWithoutConflict(string remote, string remoteAlias, string branchName)
  151. {
  152. ITracer tracer = _tracerFactory.GetTracer();
  153. try
  154. {
  155. _gitExe.Execute(tracer, @"remote add {0} ""{1}""", remoteAlias, remote);
  156. _gitExe.Execute(tracer, @"fetch {0}", remoteAlias);
  157. Update(branchName);
  158. _gitExe.Execute(tracer, @"reset --hard {0}/{1}", remoteAlias, branchName);
  159. }
  160. finally
  161. {
  162. try
  163. {
  164. _gitExe.Execute(tracer, @"remote rm {0}", remoteAlias);
  165. }
  166. catch { }
  167. }
  168. }
  169. public void Update(string id)
  170. {
  171. ITracer tracer = _tracerFactory.GetTracer();
  172. _gitExe.Execute(tracer, "checkout {0} --force", id);
  173. }
  174. public void Update()
  175. {
  176. Update("master");
  177. }
  178. public ChangeSetDetail GetDetails(string id)
  179. {
  180. string show = _gitExe.Execute("show {0} -m -p --numstat --shortstat", id).Item1;
  181. var detail = ParseShow(show.AsReader());
  182. string showStatus = _gitExe.Execute("show {0} -m --name-status --format=\"%H\"", id).Item1;
  183. var statusReader = showStatus.AsReader();
  184. // Skip the commit details
  185. statusReader.ReadLine();
  186. statusReader.SkipWhitespace();
  187. PopulateStatus(statusReader, detail);
  188. return detail;
  189. }
  190. public ChangeSetDetail GetWorkingChanges()
  191. {
  192. // Add everything so we can see a diff of the current changes
  193. var statuses = GetStatus().ToList();
  194. if (!statuses.Any())
  195. {
  196. return null;
  197. }
  198. if (IsEmpty())
  199. {
  200. return MakeNewFileDiff(statuses);
  201. }
  202. string diff = _gitExe.Execute("diff --no-ext-diff -p --numstat --shortstat head").Item1;
  203. var detail = ParseShow(diff.AsReader(), includeChangeSet: false);
  204. foreach (var fileStatus in statuses)
  205. {
  206. FileInfo fileInfo;
  207. if (detail.Files.TryGetValue(fileStatus.Path, out fileInfo))
  208. {
  209. fileInfo.Status = fileStatus.Status;
  210. }
  211. }
  212. return detail;
  213. }
  214. private ChangeSetDetail MakeNewFileDiff(IEnumerable<FileStatus> statuses)
  215. {
  216. var changeSetDetail = new ChangeSetDetail();
  217. foreach (var fileStatus in statuses)
  218. {
  219. var fileInfo = new FileInfo
  220. {
  221. Status = fileStatus.Status
  222. };
  223. foreach (var diff in CreateDiffLines(fileStatus.Path))
  224. {
  225. fileInfo.DiffLines.Add(diff);
  226. }
  227. changeSetDetail.Files[fileStatus.Path] = fileInfo;
  228. changeSetDetail.FilesChanged++;
  229. changeSetDetail.Insertions += fileInfo.DiffLines.Count;
  230. }
  231. return changeSetDetail;
  232. }
  233. private IEnumerable<LineDiff> CreateDiffLines(string path)
  234. {
  235. if (Directory.Exists(path))
  236. {
  237. return Enumerable.Empty<LineDiff>();
  238. }
  239. // TODO: Detect binary files
  240. path = Path.Combine(_gitExe.WorkingDirectory, path);
  241. var diff = new List<LineDiff>();
  242. string[] lines = File.ReadAllLines(path);
  243. foreach (var line in lines)
  244. {
  245. diff.Add(new LineDiff(ChangeType.Added, "+" + line));
  246. }
  247. return diff;
  248. }
  249. public IEnumerable<Branch> GetBranches()
  250. {
  251. if (IsEmpty())
  252. {
  253. yield break;
  254. }
  255. string branches = _gitExe.Execute("branch").Item1;
  256. var reader = branches.AsReader();
  257. string currentId = CurrentId;
  258. var branchNames = new List<string>();
  259. while (!reader.Done)
  260. {
  261. // * branchname
  262. var lineReader = reader.ReadLine().AsReader();
  263. lineReader.ReadUntilWhitespace();
  264. lineReader.SkipWhitespace();
  265. string branchName = lineReader.ReadToEnd();
  266. if (branchName.Contains("(no branch)"))
  267. {
  268. continue;
  269. }
  270. branchNames.Add(branchName.Trim());
  271. }
  272. foreach (var branchName in branchNames)
  273. {
  274. string id = _gitExe.Execute("rev-parse {0}", branchName).Item1.Trim();
  275. yield return new GitBranch(id, branchName, currentId == id);
  276. }
  277. }
  278. public void SetTraceLevel(int level)
  279. {
  280. _gitExe.SetTraceLevel(level);
  281. }
  282. private bool IsEmpty()
  283. {
  284. // REVIEW: Is this reliable
  285. return String.IsNullOrWhiteSpace(_gitExe.Execute("branch").Item1);
  286. }
  287. private IEnumerable<ChangeSet> Log(string command = "log --all", params object[] args)
  288. {
  289. if (IsEmpty())
  290. {
  291. return Enumerable.Empty<ChangeSet>();
  292. }
  293. string log = _gitExe.Execute(command, args).Item1;
  294. return ParseCommits(log.AsReader());
  295. }
  296. private IEnumerable<ChangeSet> ParseCommits(IStringReader reader)
  297. {
  298. while (!reader.Done)
  299. {
  300. yield return ParseCommit(reader);
  301. }
  302. }
  303. internal static void PopulateStatus(IStringReader reader, ChangeSetDetail detail)
  304. {
  305. while (!reader.Done)
  306. {
  307. string line = reader.ReadLine();
  308. // Status lines contain tabs
  309. if (!line.Contains("\t"))
  310. {
  311. continue;
  312. }
  313. var lineReader = line.AsReader();
  314. string status = lineReader.ReadUntilWhitespace();
  315. lineReader.SkipWhitespace();
  316. string name = lineReader.ReadToEnd().TrimEnd();
  317. lineReader.SkipWhitespace();
  318. FileInfo file;
  319. if (detail.Files.TryGetValue(name, out file))
  320. {
  321. file.Status = ConvertStatus(status);
  322. }
  323. }
  324. }
  325. private static bool IsCommitHeader(string value)
  326. {
  327. return value.StartsWith("commit ");
  328. }
  329. private ChangeSetDetail ParseShow(IStringReader reader, bool includeChangeSet = true)
  330. {
  331. ChangeSetDetail detail = null;
  332. if (includeChangeSet)
  333. {
  334. detail = ParseCommitAndSummary(reader);
  335. }
  336. else
  337. {
  338. detail = new ChangeSetDetail();
  339. ParseSummary(reader, detail);
  340. }
  341. ParseDiffAndPopulate(reader, detail);
  342. return detail;
  343. }
  344. internal static void ParseDiffAndPopulate(IStringReader reader, ChangeSetDetail detail)
  345. {
  346. foreach (var diff in ParseDiff(reader))
  347. {
  348. FileInfo stats;
  349. if (!detail.Files.TryGetValue(diff.FileName, out stats))
  350. {
  351. stats = new FileInfo();
  352. detail.Files.Add(diff.FileName, stats);
  353. }
  354. // Set the binary flag if any of the files are binary
  355. bool binary = diff.Binary || stats.Binary;
  356. stats.Binary = binary;
  357. diff.Binary = binary;
  358. foreach (var line in diff.Lines)
  359. {
  360. stats.DiffLines.Add(line);
  361. }
  362. }
  363. }
  364. internal static ChangeSetDetail ParseCommitAndSummary(IStringReader reader)
  365. {
  366. // Parse the changeset
  367. ChangeSet changeSet = ParseCommit(reader);
  368. var detail = new ChangeSetDetail(changeSet);
  369. ParseSummary(reader, detail);
  370. return detail;
  371. }
  372. internal static void ParseSummary(IStringReader reader, ChangeSetDetail detail)
  373. {
  374. reader.SkipWhitespace();
  375. while (!reader.Done)
  376. {
  377. string line = reader.ReadLine();
  378. if (ParserHelpers.IsSingleNewLine(line))
  379. {
  380. break;
  381. }
  382. else if (line.Contains('\t'))
  383. {
  384. // n n path
  385. string[] parts = line.Split('\t');
  386. int insertions;
  387. Int32.TryParse(parts[0], out insertions);
  388. int deletions;
  389. Int32.TryParse(parts[1], out deletions);
  390. string path = parts[2].TrimEnd();
  391. detail.Files[path] = new FileInfo
  392. {
  393. Insertions = insertions,
  394. Deletions = deletions,
  395. Binary = parts[0] == "-" && parts[1] == "-"
  396. };
  397. }
  398. else
  399. {
  400. // n files changed, n insertions(+), n deletions(-)
  401. ParserHelpers.ParseSummaryFooter(line, detail);
  402. }
  403. }
  404. }
  405. internal static IEnumerable<FileDiff> ParseDiff(IStringReader reader)
  406. {
  407. var builder = new StringBuilder();
  408. // If this was a merge change set then we'll parse the details out of the
  409. // first diff
  410. ChangeSetDetail merge = null;
  411. do
  412. {
  413. string line = reader.ReadLine();
  414. // If we see a new diff header then process the previous diff if any
  415. if ((reader.Done || IsDiffHeader(line)) && builder.Length > 0)
  416. {
  417. if (reader.Done)
  418. {
  419. builder.Append(line);
  420. }
  421. string diffChunk = builder.ToString();
  422. FileDiff diff = ParseDiffChunk(diffChunk.AsReader(), ref merge);
  423. if (diff != null)
  424. {
  425. yield return diff;
  426. }
  427. builder.Clear();
  428. }
  429. if (!reader.Done)
  430. {
  431. builder.Append(line);
  432. }
  433. } while (!reader.Done);
  434. }
  435. internal static bool IsDiffHeader(string line)
  436. {
  437. return line.StartsWith("diff --git", StringComparison.InvariantCulture);
  438. }
  439. internal static FileDiff ParseDiffChunk(IStringReader reader, ref ChangeSetDetail merge)
  440. {
  441. var diff = ParseDiffHeader(reader, merge);
  442. if (diff == null)
  443. {
  444. return null;
  445. }
  446. // Current diff range
  447. DiffRange currentRange = null;
  448. int? leftCounter = null;
  449. int? rightCounter = null;
  450. // Parse the file diff
  451. while (!reader.Done)
  452. {
  453. int? currentLeft = null;
  454. int? currentRight = null;
  455. string line = reader.ReadLine();
  456. if (line.Equals(@"\ No newline at end of file", StringComparison.OrdinalIgnoreCase))
  457. {
  458. continue;
  459. }
  460. bool isDiffRange = line.StartsWith("@@");
  461. ChangeType? changeType = null;
  462. if (line.StartsWith("+"))
  463. {
  464. changeType = ChangeType.Added;
  465. currentRight = ++rightCounter;
  466. currentLeft = null;
  467. }
  468. else if (line.StartsWith("-"))
  469. {
  470. changeType = ChangeType.Deleted;
  471. currentLeft = ++leftCounter;
  472. currentRight = null;
  473. }
  474. else if (IsCommitHeader(line))
  475. {
  476. reader.PutBack(line.Length);
  477. merge = ParseCommitAndSummary(reader);
  478. }
  479. else
  480. {
  481. if (!isDiffRange)
  482. {
  483. currentLeft = ++leftCounter;
  484. currentRight = ++rightCounter;
  485. }
  486. changeType = ChangeType.None;
  487. }
  488. if (changeType != null)
  489. {
  490. var lineDiff = new LineDiff(changeType.Value, line);
  491. if (!isDiffRange)
  492. {
  493. lineDiff.LeftLine = currentLeft;
  494. lineDiff.RightLine = currentRight;
  495. }
  496. diff.Lines.Add(lineDiff);
  497. }
  498. if (isDiffRange)
  499. {
  500. // Parse the new diff range
  501. currentRange = DiffRange.Parse(line.AsReader());
  502. leftCounter = currentRange.LeftFrom - 1;
  503. rightCounter = currentRange.RightFrom - 1;
  504. }
  505. }
  506. return diff;
  507. }
  508. private static FileDiff ParseDiffHeader(IStringReader reader, ChangeSetDetail merge)
  509. {
  510. string fileName = ParseFileName(reader.ReadLine());
  511. bool binary = false;
  512. while (!reader.Done)
  513. {
  514. string line = reader.ReadLine();
  515. if (line.StartsWith("@@"))
  516. {
  517. reader.PutBack(line.Length);
  518. break;
  519. }
  520. else if (line.StartsWith("GIT binary patch"))
  521. {
  522. binary = true;
  523. }
  524. }
  525. if (binary)
  526. {
  527. // Skip binary files
  528. reader.ReadToEnd();
  529. }
  530. var diff = new FileDiff(fileName)
  531. {
  532. Binary = binary
  533. };
  534. // Skip files from merged changesets
  535. if (merge != null && merge.Files.ContainsKey(fileName))
  536. {
  537. return null;
  538. }
  539. return diff;
  540. }
  541. internal static string ParseFileName(string diffHeader)
  542. {
  543. // Get rid of the diff header (git --diff)
  544. diffHeader = diffHeader.TrimEnd().Substring(diffHeader.IndexOf("a/"));
  545. // the format is always a/{file name} b/{file name}
  546. int mid = diffHeader.Length / 2;
  547. return diffHeader.Substring(0, mid).Substring(2);
  548. }
  549. internal static ChangeSet ParseCommit(IStringReader reader)
  550. {
  551. // commit hash
  552. reader.ReadUntilWhitespace();
  553. reader.SkipWhitespace();
  554. string id = reader.ReadUntilWhitespace();
  555. // Merges will have (from hash) so we're skipping that
  556. reader.ReadLine();
  557. string author = null;
  558. string email = null;
  559. string date = null;
  560. while (!reader.Done)
  561. {
  562. string line = reader.ReadLine();
  563. if (ParserHelpers.IsSingleNewLine(line))
  564. {
  565. break;
  566. }
  567. var subReader = line.AsReader();
  568. string key = subReader.ReadUntil(':');
  569. // Skip :
  570. subReader.Skip();
  571. subReader.SkipWhitespace();
  572. string value = subReader.ReadToEnd().Trim();
  573. if (key.Equals("Author", StringComparison.OrdinalIgnoreCase))
  574. {
  575. // Author <email>
  576. var authorReader = value.AsReader();
  577. author = authorReader.ReadUntil('<').Trim();
  578. authorReader.Skip();
  579. email = authorReader.ReadUntil('>');
  580. }
  581. else if (key.Equals("Date", StringComparison.OrdinalIgnoreCase))
  582. {
  583. date = value;
  584. }
  585. }
  586. var messageBuilder = new StringBuilder();
  587. while (!reader.Done)
  588. {
  589. string line = reader.ReadLine();
  590. if (ParserHelpers.IsSingleNewLine(line))
  591. {
  592. break;
  593. }
  594. messageBuilder.Append(line);
  595. }
  596. string message = messageBuilder.ToString();
  597. return new ChangeSet(id, author, email, message, DateTimeOffset.ParseExact(date, "ddd MMM d HH:mm:ss yyyy zzz", CultureInfo.InvariantCulture));
  598. }
  599. internal static IEnumerable<FileStatus> ParseStatus(IStringReader reader)
  600. {
  601. reader.SkipWhitespace();
  602. while (!reader.Done)
  603. {
  604. var subReader = reader.ReadLine().AsReader();
  605. string status = subReader.ReadUntilWhitespace().Trim();
  606. string path = subReader.ReadLine().Trim();
  607. yield return new FileStatus(path, ConvertStatus(status));
  608. reader.SkipWhitespace();
  609. }
  610. }
  611. internal static ChangeType ConvertStatus(string status)
  612. {
  613. switch (status)
  614. {
  615. case "A":
  616. case "AM":
  617. return ChangeType.Added;
  618. case "M":
  619. case "MM":
  620. return ChangeType.Modified;
  621. case "AD":
  622. case "D":
  623. return ChangeType.Deleted;
  624. case "R":
  625. return ChangeType.Renamed;
  626. case "??":
  627. return ChangeType.Untracked;
  628. default:
  629. break;
  630. }
  631. throw new InvalidOperationException("Unsupported status " + status);
  632. }
  633. internal class DiffRange
  634. {
  635. public int LeftFrom { get; set; }
  636. public int LeftTo { get; set; }
  637. public int RightFrom { get; set; }
  638. public int RightTo { get; set; }
  639. public static DiffRange Parse(IStringReader reader)
  640. {
  641. var range = new DiffRange();
  642. reader.Skip("@@");
  643. reader.SkipWhitespace();
  644. reader.Skip('-');
  645. range.LeftFrom = reader.ReadInt();
  646. if (reader.Skip(','))
  647. {
  648. range.LeftTo = range.LeftFrom + reader.ReadInt();
  649. }
  650. else
  651. {
  652. range.LeftTo = range.LeftFrom;
  653. }
  654. reader.SkipWhitespace();
  655. reader.Skip('+');
  656. range.RightFrom = reader.ReadInt();
  657. if (reader.Skip(','))
  658. {
  659. range.RightTo = range.RightFrom + reader.ReadInt();
  660. }
  661. else
  662. {
  663. range.RightTo = range.RightFrom;
  664. }
  665. reader.SkipWhitespace();
  666. reader.Skip("@@");
  667. return range;
  668. }
  669. }
  670. }
  671. }