2 * Copyright (C) 2009, Henon <meinrad.recheis@gmail.com>
6 * Redistribution and use in source and binary forms, with or
7 * without modification, are permitted provided that the following
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
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.
39 using System
.Collections
.Generic
;
42 using GitSharp
.Commands
;
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
;
52 using GitSharp
.Core
.TreeWalk
;
53 using GitSharp
.Core
.TreeWalk
.Filter
;
54 using System
.Diagnostics
;
55 using System
.Collections
;
60 /// Represents a revision of the files and directories tracked in the repository.
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
)
86 private Core
.Commit _internal_commit
;
88 private Core
.Commit InternalCommit
92 if (_internal_commit
== null)
95 _internal_commit
= _repo
._internal_repo
.MapCommit(_id
);
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
112 return InternalCommit
is Core
.Commit
;
117 /// The commit message.
119 public string Message
123 if (InternalCommit
== null) // this might happen if the object was created with an incorrect reference
125 return InternalCommit
.Message
;
130 /// The encoding of the commit details.
132 public Encoding Encoding
136 if (InternalCommit
== null) // this might happen if the object was created with an incorrect reference
138 return InternalCommit
.Encoding
;
143 /// The author of the change set represented by this commit.
149 if (InternalCommit
== null) // this might happen if the object was created with an incorrect reference
151 return new Author() { Name = InternalCommit.Author.Name, EmailAddress = InternalCommit.Author.EmailAddress }
;
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.
158 public Author Committer
162 if (InternalCommit
== null) // this might happen if the object was created with an incorrect reference
164 var committer
= InternalCommit
.Committer
;
165 if (committer
== null) // this is null if the author committed himself
167 return new Author() { Name = committer.Name, EmailAddress = committer.EmailAddress }
;
172 /// Original timestamp of the commit created by Author.
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
);
185 /// Final timestamp of the commit, after Committer has re-committed Author's commit.
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
);
201 /// Returns true if the commit was created by the author of the change set himself.
203 public bool IsCommittedByAuthor
207 return Author
== Committer
;
212 /// Returns all parent commits.
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();
225 /// True if the commit has at least one parent.
227 public bool HasParents
231 if (InternalCommit
== null) // this might happen if the object was created with an incorrect reference
233 return InternalCommit
.ParentIds
.Length
> 0;
238 /// The first parent commit if the commit has at least one parent, null otherwise.
244 if (InternalCommit
== null) // this might happen if the object was created with an incorrect reference
247 return new Commit(_repo
, InternalCommit
.ParentIds
[0]);
253 /// The commit's reference to the root of the directory structure of the revision.
259 if (InternalCommit
== null) // this might happen if the object was created with an incorrect reference
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.
273 /// Returns an iterator over all ancestor-commits of this commit.
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
));
284 from revcommit
in revwalk
.Skip(1) // skip this commit
285 select new Commit(_repo
, revcommit
.AsCommit(revwalk
));
296 /// Checkout this commit into the working directory. Does not change HEAD.
298 /// <seealso cref="Branch.Checkout"/> and <seealso cref="Index.Checkout()"/>.
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
);
322 /// Check out the given paths into the working directory. Files in the working directory will be overwritten.
324 /// See also <seealso cref="Index.Checkout(string[])"/> to check out paths from the index.
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
;
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?
349 public Commit
[] Blame (string path
)
351 Leaf leaf
= Tree
[path
] as Leaf
;
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)
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
;
380 if (cleaf
== null || emptyLines
<= 0)
383 prevCommit
= ancestor
;
386 for (int n
=0; n
<lines
.Length
; n
++)
387 if (lines
[n
] == null)
388 lines
[n
] = prevAncestor
;
394 #region --> Diffing commits
398 /// Compare reference commit against compared commit. You may pass in a null commit (i.e. for getting the changes of the first commit)
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)
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
);
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 }
);
424 /// compare the given commits and return the changes.
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
)
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));
465 change
.ChangeType
= ChangeType
.Modified
;
466 change
.ReferenceObject
= Wrap(repo
, walk
.getObjectId(0));
467 change
.ComparedObject
= Wrap(repo
, walk
.getObjectId(1));
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));
489 else if (m0
== 0 && raw_modes
.Any(m1
=> m1
!= 0))
491 change
.ChangeType
= ChangeType
.Deleted
;
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));
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));
511 /// Compares this commit against another one and returns all changes between the two.
513 /// <param name="other"></param>
514 /// <returns></returns>
515 public IEnumerable
<Change
> CompareAgainst(Commit other
)
517 return CompareCommits(this, other
);
521 /// Returns the changes of this commit vs. it's parent commit(s). Works for the first commit too.
523 public IEnumerable
<Change
> Changes
527 if (this.Parents
.Count() == 0)
528 return CompareCommits(this, null);
530 return CompareAgainstParents();
535 /// Compare a commit against (one or multiple) parents (merge scenario)
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());
553 #region --> Committing
556 public static Commit
Create(string message
, Commit parent
, Tree tree
)
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");
580 throw new ArgumentException("tree must not be null");
581 var repo
= tree
.Repository
;
582 var corecommit
= new Core
.Commit(repo
._internal_repo
);
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
);
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
602 return Charset
.forName(encodingAlias
);
608 public static implicit operator Core
.Commit(Commit c
)
610 return c
.InternalCommit
;
613 public override string ToString()
615 return "Commit[" + ShortHash
+ "]";