Update README.txt
[GitSharp.git] / GitSharp / Commit.cs
blob2aabf5b4870d60735236816ebd799a01e0aeb172
1 /*
2 * Copyright (C) 2009, Henon <meinrad.recheis@gmail.com>
4 * All rights reserved.
6 * Redistribution and use in source and binary forms, with or
7 * without modification, are permitted provided that the following
8 * conditions are met:
10 * - Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
13 * - Redistributions in binary form must reproduce the above
14 * copyright notice, this list of conditions and the following
15 * disclaimer in the documentation and/or other materials provided
16 * with the distribution.
18 * - Neither the name of the Git Development Community nor the
19 * names of its contributors may be used to endorse or promote
20 * products derived from this software without specific prior
21 * written permission.
23 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
24 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
25 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
26 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
27 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
28 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
29 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
30 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
31 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
32 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
33 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
34 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
35 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38 using System;
39 using System.Collections.Generic;
40 using System.Linq;
41 using System.Text;
42 using GitSharp.Commands;
43 using GitSharp.Core;
44 using GitSharp.Core.RevWalk;
45 using GitSharp.Core.Util;
46 using GitSharp.Core.Util.JavaHelper;
47 using GitSharp.Core.Diff;
48 using ObjectId = GitSharp.Core.ObjectId;
49 using CoreRef = GitSharp.Core.Ref;
50 using CoreTree = GitSharp.Core.Tree;
51 using System.IO;
52 using GitSharp.Core.TreeWalk;
53 using GitSharp.Core.TreeWalk.Filter;
54 using System.Diagnostics;
55 using System.Collections;
57 namespace GitSharp
59 /// <summary>
60 /// Represents a revision of the files and directories tracked in the repository.
61 /// </summary>
62 public class Commit : AbstractObject
65 public Commit(Repository repo, string hash_or_name)
66 : base(repo, hash_or_name)
70 internal Commit(Repository repo, CoreRef @ref)
71 : base(repo, @ref.ObjectId)
75 internal Commit(Repository repo, Core.Commit internal_commit)
76 : base(repo, internal_commit.CommitId)
78 _internal_commit = internal_commit;
81 internal Commit(Repository repo, ObjectId id)
82 : base(repo, id)
86 private Core.Commit _internal_commit;
88 private Core.Commit InternalCommit
90 get
92 if (_internal_commit == null)
93 try
95 _internal_commit = _repo._internal_repo.MapCommit(_id);
97 catch (Exception)
99 // the commit object is invalid. however, we can not allow exceptions here because they would not be expected.
101 return _internal_commit;
105 #region --> Commit Properties
108 public bool IsValid
112 return InternalCommit is Core.Commit;
116 /// <summary>
117 /// The commit message.
118 /// </summary>
119 public string Message
123 if (InternalCommit == null) // this might happen if the object was created with an incorrect reference
124 return null;
125 return InternalCommit.Message;
129 /// <summary>
130 /// The encoding of the commit details.
131 /// </summary>
132 public Encoding Encoding
136 if (InternalCommit == null) // this might happen if the object was created with an incorrect reference
137 return null;
138 return InternalCommit.Encoding;
142 /// <summary>
143 /// The author of the change set represented by this commit.
144 /// </summary>
145 public Author Author
149 if (InternalCommit == null) // this might happen if the object was created with an incorrect reference
150 return null;
151 return new Author() { Name = InternalCommit.Author.Name, EmailAddress = InternalCommit.Author.EmailAddress };
155 /// <summary>
156 /// The person who committed the change set by reusing authorship information from another commit. If the commit was created by the author himself, Committer is equal to the Author.
157 /// </summary>
158 public Author Committer
162 if (InternalCommit == null) // this might happen if the object was created with an incorrect reference
163 return null;
164 var committer = InternalCommit.Committer;
165 if (committer == null) // this is null if the author committed himself
166 return Author;
167 return new Author() { Name = committer.Name, EmailAddress = committer.EmailAddress };
171 /// <summary>
172 /// Original timestamp of the commit created by Author.
173 /// </summary>
174 public DateTimeOffset AuthorDate
178 if (InternalCommit == null) // this might happen if the object was created with an incorrect reference
179 return DateTimeOffset.MinValue;
180 return InternalCommit.Author.When.MillisToDateTimeOffset(InternalCommit.Author.TimeZoneOffset);
184 /// <summary>
185 /// Final timestamp of the commit, after Committer has re-committed Author's commit.
186 /// </summary>
187 public DateTimeOffset CommitDate
191 if (InternalCommit == null) // this might happen if the object was created with an incorrect reference
192 return DateTimeOffset.MinValue;
193 var committer = InternalCommit.Committer;
194 if (committer == null) // this is null if the author committed himself
195 committer = InternalCommit.Author;
196 return committer.When.MillisToDateTimeOffset(committer.TimeZoneOffset);
200 /// <summary>
201 /// Returns true if the commit was created by the author of the change set himself.
202 /// </summary>
203 public bool IsCommittedByAuthor
207 return Author == Committer;
211 /// <summary>
212 /// Returns all parent commits.
213 /// </summary>
214 public IEnumerable<Commit> Parents
218 if (InternalCommit == null) // this might happen if the object was created with an incorrect reference
219 return new Commit[0];
220 return InternalCommit.ParentIds.Select(parent_id => new Commit(_repo, parent_id)).ToArray();
224 /// <summary>
225 /// True if the commit has at least one parent.
226 /// </summary>
227 public bool HasParents
231 if (InternalCommit == null) // this might happen if the object was created with an incorrect reference
232 return false;
233 return InternalCommit.ParentIds.Length > 0;
237 /// <summary>
238 /// The first parent commit if the commit has at least one parent, null otherwise.
239 /// </summary>
240 public Commit Parent
244 if (InternalCommit == null) // this might happen if the object was created with an incorrect reference
245 return null;
246 if (HasParents)
247 return new Commit(_repo, InternalCommit.ParentIds[0]);
248 return null;
252 /// <summary>
253 /// The commit's reference to the root of the directory structure of the revision.
254 /// </summary>
255 public Tree Tree
259 if (InternalCommit == null) // this might happen if the object was created with an incorrect reference
260 return null;
263 return new Tree(_repo, InternalCommit.TreeEntry);
265 catch (GitSharp.Core.Exceptions.MissingObjectException)
267 return null; // relieve the client of having to catch the exception! If tree is null it is obvious that the tree could not be found.
272 /// <summary>
273 /// Returns an iterator over all ancestor-commits of this commit.
274 /// </summary>
275 public IEnumerable<Commit> Ancestors
279 var revwalk = new RevWalk(_repo);
280 revwalk.RevSortStrategy.Add(RevSort.Strategy.COMMIT_TIME_DESC);
281 revwalk.RevSortStrategy.Add(RevSort.Strategy.TOPO);
282 revwalk.markStart(revwalk.parseCommit(_id));
283 return
284 from revcommit in revwalk.Skip(1) // skip this commit
285 select new Commit(_repo, revcommit.AsCommit(revwalk));
290 #endregion
292 #region --> Checkout
295 /// <summary>
296 /// Checkout this commit into the working directory. Does not change HEAD.
297 /// <para/>
298 /// <seealso cref="Branch.Checkout"/> and <seealso cref="Index.Checkout()"/>.
299 /// </summary>
300 public void Checkout()
302 Checkout(_repo.WorkingDirectory);
305 private void Checkout(string working_directory) // [henon] made this private to not confuse with Checkout( paths ). It seems to be not a common use case anyway and could be better exposed via the CheckoutCommand
307 if (InternalCommit == null)
308 throw new InvalidOperationException("Unable to checkout this commit. It was not initialized properly (i.e. the hash is not pointing to a commit object).");
309 if (working_directory == null)
310 throw new ArgumentException("Path to checkout directory must not be null");
311 if (new DirectoryInfo(working_directory).Exists == false)
312 throw new IOException("Cannot checkout into non-existent directory: " + working_directory);
313 var db = _repo._internal_repo;
314 var index = _repo.Index.GitIndex;
315 index.RereadIfNecessary();
316 CoreTree tree = InternalCommit.TreeEntry;
317 var co = new GitSharp.Core.WorkDirCheckout(db, new DirectoryInfo(working_directory), index, tree);
318 co.checkout();
321 /// <summary>
322 /// Check out the given paths into the working directory. Files in the working directory will be overwritten.
323 /// <para/>
324 /// See also <seealso cref="Index.Checkout(string[])"/> to check out paths from the index.
325 /// </summary>
326 /// <param name="paths">Relative paths of the files to check out.</param>
327 /// Throws a lot of IO and Security related exceptions.
328 public void Checkout(params string[] paths)
330 var tree = this.Tree;
331 if (tree == null || !tree.IsTree)
332 throw new InvalidOperationException("This commit doesn't seem to have a valid tree.");
333 foreach (string path in paths)
335 var leaf = tree[path] as Leaf;
336 if (leaf == null)
337 throw new ArgumentException("The given path does not exist in this commit: " + path);
338 var filename = Path.Combine(_repo.WorkingDirectory, path);
339 new FileInfo(filename).Directory.Mkdirs();
340 File.WriteAllBytes(filename, leaf.Blob.RawData); // todo: hmm, what is with file permissions and other file attributes?
345 #endregion
347 #region --> Blame
349 public Commit[] Blame (string path)
351 Leaf leaf = Tree [path] as Leaf;
352 if (leaf == null)
353 throw new ArgumentException("The given path does not exist in this commit: " + path);
354 byte[] data = leaf.RawData;
355 int lineCount = RawParseUtils.lineMap (data, 0, data.Length).size ();
356 Commit[] lines = new Commit [lineCount];
357 var curText = new RawText (data);
358 Commit prevAncestor = this;
360 Leaf prevLeaf = null;
361 Commit prevCommit = null;
362 int emptyLines = lineCount;
364 foreach (Commit ancestor in Ancestors) {
365 Leaf cleaf = ancestor.Tree [path] as Leaf;
366 if (prevCommit != null && (cleaf == null || cleaf.Hash != prevLeaf.Hash)) {
367 byte[] prevData = prevLeaf.RawData;
368 if (prevData == null)
369 break;
370 var prevText = new RawText (prevData);
371 var differ = new MyersDiff (prevText, curText);
372 foreach (Edit e in differ.getEdits ()) {
373 for (int n = e.BeginB; n < e.EndB; n++) {
374 if (lines [n] == null) {
375 lines [n] = prevCommit;
376 emptyLines--;
380 if (cleaf == null || emptyLines <= 0)
381 break;
383 prevCommit = ancestor;
384 prevLeaf = cleaf;
386 for (int n=0; n<lines.Length; n++)
387 if (lines [n] == null)
388 lines [n] = prevAncestor;
389 return lines;
392 #endregion
394 #region --> Diffing commits
397 /// <summary>
398 /// Compare reference commit against compared commit. You may pass in a null commit (i.e. for getting the changes of the first commit)
399 /// </summary>
400 /// <param name="reference">Usually the more recent commit</param>
401 /// <param name="compared">Usually an ancestor of the reference commit</param>
402 /// <returns>a list of changes (<see cref="Change"/>)</returns>
403 public static IEnumerable<Change> CompareCommits(Commit reference, Commit compared)
405 var changes = new List<Change>();
406 if (reference == null && compared == null)
407 return changes;
408 var repo = (reference ?? compared).Repository;
409 var ref_tree = (reference != null ? reference.Tree._id : ObjectId.ZeroId);
410 var compared_tree = (compared != null ? compared.Tree._id : ObjectId.ZeroId);
411 var db = repo._internal_repo;
412 var walk = new TreeWalk(db);
413 if (reference == null || compared == null)
414 walk.reset((reference ?? compared).Tree._id);
415 else
416 walk.reset(new GitSharp.Core.AnyObjectId[] {ref_tree, compared_tree});
417 walk.Recursive = true;
418 walk.setFilter(AndTreeFilter.create(TreeFilter.ANY_DIFF, TreeFilter.ALL));
420 return CalculateCommitDiff(repo, walk, new[] { reference, compared });
423 /// <summary>
424 /// compare the given commits and return the changes.
425 /// </summary>
426 /// <param name="repo"></param>
427 /// <param name="walk"></param>
428 /// <param name="commits">first commit in the array is the commit that is compared against the others which are his ancestors (i.e. from different branches)</param>
429 /// <returns></returns>
430 private static IEnumerable<Change> CalculateCommitDiff(Repository repo, TreeWalk walk, Commit[] commits)
432 while (walk.next())
434 int m0 = walk.getRawMode(0);
435 if (walk.getTreeCount() == 2)
437 int m1 = walk.getRawMode(1);
438 var change = new Change
440 ReferenceCommit = commits[0],
441 ComparedCommit = commits[1],
442 ReferencePermissions = walk.getFileMode(0).Bits,
443 ComparedPermissions = walk.getFileMode(1).Bits,
444 Name = walk.getNameString(),
445 Path = walk.getPathString(),
447 if (m0 != 0 && m1 == 0)
449 change.ChangeType = ChangeType.Added;
450 change.ComparedObject = Wrap(repo, walk.getObjectId(0));
452 else if (m0 == 0 && m1 != 0)
454 change.ChangeType = ChangeType.Deleted;
455 change.ReferenceObject = Wrap(repo, walk.getObjectId(0));
457 else if (m0 != m1 && walk.idEqual(0, 1))
459 change.ChangeType = ChangeType.TypeChanged;
460 change.ReferenceObject = Wrap(repo, walk.getObjectId(0));
461 change.ComparedObject = Wrap(repo, walk.getObjectId(1));
463 else
465 change.ChangeType = ChangeType.Modified;
466 change.ReferenceObject = Wrap(repo, walk.getObjectId(0));
467 change.ComparedObject = Wrap(repo, walk.getObjectId(1));
469 yield return change;
471 else
473 var raw_modes = new int[walk.getTreeCount()-1];
474 for(int i = 0;i<walk.getTreeCount()-1; i++)
475 raw_modes[i] = walk.getRawMode(i+1);
476 var change = new Change
478 ReferenceCommit = commits[0],
479 //ComparedCommit = compared,
480 Name = walk.getNameString(),
481 Path = walk.getPathString(),
483 if (m0 != 0 && raw_modes.All(m1 => m1 == 0))
485 change.ChangeType = ChangeType.Added;
486 change.ComparedObject = Wrap(repo, walk.getObjectId(0));
487 yield return change;
489 else if (m0 == 0 && raw_modes.Any(m1 => m1 != 0))
491 change.ChangeType = ChangeType.Deleted;
492 yield return change;
494 else if (raw_modes.Select((m1, i) => new { Mode = m1, Index = i + 1 }).All(x => !walk.idEqual(0, x.Index))) // TODO: not sure if this condition suffices in some special cases.
496 change.ChangeType = ChangeType.Modified;
497 change.ReferenceObject = Wrap(repo, walk.getObjectId(0));
498 yield return change;
500 else if (raw_modes.Select((m1, i) => new { Mode = m1, Index = i + 1 }).Any(x => m0 != x.Mode && walk.idEqual(0, x.Index)))
502 change.ChangeType = ChangeType.TypeChanged;
503 change.ReferenceObject = Wrap(repo, walk.getObjectId(0));
504 yield return change;
510 /// <summary>
511 /// Compares this commit against another one and returns all changes between the two.
512 /// </summary>
513 /// <param name="other"></param>
514 /// <returns></returns>
515 public IEnumerable<Change> CompareAgainst(Commit other)
517 return CompareCommits(this, other);
520 /// <summary>
521 /// Returns the changes of this commit vs. it's parent commit(s). Works for the first commit too.
522 /// </summary>
523 public IEnumerable<Change> Changes
527 if (this.Parents.Count() == 0)
528 return CompareCommits(this, null);
529 else
530 return CompareAgainstParents();
534 /// <summary>
535 /// Compare a commit against (one or multiple) parents (merge scenario)
536 /// </summary>
537 /// <returns>Changes of the commit</returns>
538 private IEnumerable<Change> CompareAgainstParents()
540 var db = _repo._internal_repo;
541 var tree_ids = new[] { this }.Concat(Parents).Select(c => c.Tree._id).ToArray();
542 var walk = new TreeWalk(db);
543 walk.reset(tree_ids);
544 walk.Recursive = true;
545 walk.setFilter(AndTreeFilter.create(TreeFilter.ANY_DIFF, TreeFilter.ALL));
547 return CalculateCommitDiff(_repo, walk, new[] { this }.Concat(Parents).ToArray());
551 #endregion
553 #region --> Committing
556 public static Commit Create(string message, Commit parent, Tree tree)
558 if (tree == null)
559 throw new ArgumentException("tree must not be null");
560 var repo = tree.Repository;
561 var author = Author.GetDefaultAuthor(parent._repo);
562 return Create(message, parent, tree, author, author, DateTimeOffset.Now);
565 public static Commit Create(string message, Commit parent, Tree tree, Author author)
567 return Create(message, parent, tree, author, author, DateTimeOffset.Now);
570 public static Commit Create(string message, Commit parent, Tree tree, Author author, Author committer, DateTimeOffset time)
572 return Create(message, (parent == null ? new Commit[0] : new[] { parent }), tree, author, committer, time);
575 public static Commit Create(string message, IEnumerable<Commit> parents, Tree tree, Author author, Author committer, DateTimeOffset time)
577 if (string.IsNullOrEmpty(message))
578 throw new ArgumentException("message must not be null or empty");
579 if (tree == null)
580 throw new ArgumentException("tree must not be null");
581 var repo = tree.Repository;
582 var corecommit = new Core.Commit(repo._internal_repo);
583 if (parents != null)
584 corecommit.ParentIds = parents.Select(parent => parent._id).ToArray();
585 corecommit.Author = new Core.PersonIdent(author.Name, author.EmailAddress, time.ToMillisecondsSinceEpoch(), (int)time.Offset.TotalMinutes);
586 corecommit.Committer = new Core.PersonIdent(committer.Name, committer.EmailAddress, time.ToMillisecondsSinceEpoch(), (int)time.Offset.TotalMinutes);
587 corecommit.Message = message;
588 corecommit.TreeEntry = tree.InternalTree;
589 corecommit.Encoding = GetCommitEncoding(repo);
590 corecommit.Save();
591 return new Commit(repo, corecommit);
594 private static Encoding GetCommitEncoding(Repository repository)
596 string encodingAlias = repository.Config["i18n.commitencoding"];
597 if (encodingAlias == null)
599 // No commitencoding has been specified in the config
600 return null;
602 return Charset.forName(encodingAlias);
606 #endregion
608 public static implicit operator Core.Commit(Commit c)
610 return c.InternalCommit;
613 public override string ToString()
615 return "Commit[" + ShortHash + "]";