2
This code is derived from jgit (http://eclipse.org/jgit).
3
Copyright owners are documented in jgit's IP log.
5
This program and the accompanying materials are made available
6
under the terms of the Eclipse Distribution License v1.0 which
7
accompanies this distribution, is reproduced below, and is
8
available at http://www.eclipse.org/org/documents/edl-v10.php
12
Redistribution and use in source and binary forms, with or
13
without modification, are permitted provided that the following
16
- Redistributions of source code must retain the above copyright
17
notice, this list of conditions and the following disclaimer.
19
- Redistributions in binary form must reproduce the above
20
copyright notice, this list of conditions and the following
21
disclaimer in the documentation and/or other materials provided
22
with the distribution.
24
- Neither the name of the Eclipse Foundation, Inc. nor the
25
names of its contributors may be used to endorse or promote
26
products derived from this software without specific prior
29
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
44
using System.Collections.Generic;
48
using NGit.Api.Errors;
59
/// A class used to execute a
60
/// <code>Commit</code>
61
/// command. It has setters for all
62
/// supported options and arguments of this command and a
63
/// <see cref="Call()">Call()</see>
65
/// to finally execute the command.
68
/// * href="http://www.kernel.org/pub/software/scm/git/docs/git-commit.html"
69
/// * >Git documentation about Commit</a></seealso>
70
public class CommitCommand : GitCommand<RevCommit>
72
private PersonIdent author;
74
private PersonIdent committer;
76
private string message;
80
private IList<string> only = new AList<string>();
82
private bool[] onlyProcessed;
86
private bool insertChangeId;
88
/// <summary>parents this commit should have.</summary>
90
/// parents this commit should have. The current HEAD will be in this list
91
/// and also all commits mentioned in .git/MERGE_HEAD
93
private IList<ObjectId> parents = new List<ObjectId>();
95
/// <param name="repo"></param>
96
protected internal CommitCommand(Repository repo) : base(repo)
102
/// <code>commit</code>
103
/// command with all the options and parameters
104
/// collected by the setter methods of this class. Each instance of this
105
/// class should only be used for one invocation of the command (means: one
107
/// <see cref="Call()">Call()</see>
112
/// <see cref="NGit.Revwalk.RevCommit">NGit.Revwalk.RevCommit</see>
113
/// object representing the successful commit.
115
/// <exception cref="NGit.Api.Errors.NoHeadException">when called on a git repo without a HEAD reference
117
/// <exception cref="NGit.Api.Errors.NoMessageException">when called without specifying a commit message
119
/// <exception cref="NGit.Errors.UnmergedPathException">when the current index contained unmerged paths (conflicts)
121
/// <exception cref="NGit.Api.Errors.WrongRepositoryStateException">when repository is not in the right state for committing
123
/// <exception cref="NGit.Api.Errors.JGitInternalException">
124
/// a low-level exception of JGit has occurred. The original
125
/// exception can be retrieved by calling
126
/// <see cref="System.Exception.InnerException()">System.Exception.InnerException()</see>
128
/// <code>IOException's</code>
129
/// to be wrapped. Subclasses of
130
/// <see cref="System.IO.IOException">System.IO.IOException</see>
132
/// <see cref="NGit.Errors.UnmergedPathException">NGit.Errors.UnmergedPathException</see>
134
/// typically not wrapped here but thrown as original exception
136
/// <exception cref="NGit.Api.Errors.ConcurrentRefUpdateException"></exception>
137
public override RevCommit Call()
140
RepositoryState state = repo.GetRepositoryState();
141
if (!state.CanCommit())
143
throw new WrongRepositoryStateException(MessageFormat.Format(JGitText.Get().cannotCommitOnARepoWithState
146
ProcessOptions(state);
149
if (all && !repo.IsBare && repo.WorkTree != null)
151
Git git = new Git(repo);
154
git.Add().AddFilepattern(".").SetUpdate(true).Call();
156
catch (NoFilepatternException e)
158
// should really not happen
159
throw new JGitInternalException(e.Message, e);
162
Ref head = repo.GetRef(Constants.HEAD);
165
throw new NoHeadException(JGitText.Get().commitOnRepoWithoutHEADCurrentlyNotSupported
168
// determine the current HEAD and the commit it is referring to
169
ObjectId headId = repo.Resolve(Constants.HEAD + "^{commit}");
174
RevCommit previousCommit = new RevWalk(repo).ParseCommit(headId);
175
RevCommit[] p = previousCommit.Parents;
176
for (int i = 0; i < p.Length; i++)
178
parents.Add(0, p[i].Id);
183
parents.Add(0, headId);
187
DirCache index = repo.LockDirCache();
192
index = CreateTemporaryIndex(headId, index);
194
ObjectInserter odi = repo.NewObjectInserter();
197
// Write the index as tree to the object database. This may
198
// fail for example when the index contains unmerged paths
199
// (unresolved conflicts)
200
ObjectId indexTreeId = index.WriteTree(odi);
203
InsertChangeId(indexTreeId);
205
// Create a Commit object, populate it and write it
206
NGit.CommitBuilder commit = new NGit.CommitBuilder();
207
commit.Committer = committer;
208
commit.Author = author;
209
commit.Message = message;
210
commit.SetParentIds(parents);
211
commit.TreeId = indexTreeId;
212
ObjectId commitId = odi.Insert(commit);
214
RevWalk revWalk = new RevWalk(repo);
217
RevCommit revCommit = revWalk.ParseCommit(commitId);
218
RefUpdate ru = repo.UpdateRef(Constants.HEAD);
219
ru.SetNewObjectId(commitId);
220
string prefix = amend ? "commit (amend): " : "commit: ";
221
ru.SetRefLogMessage(prefix + revCommit.GetShortMessage(), false);
222
ru.SetExpectedOldObjectId(headId);
223
RefUpdate.Result rc = ru.ForceUpdate();
226
case RefUpdate.Result.NEW:
227
case RefUpdate.Result.FORCED:
228
case RefUpdate.Result.FAST_FORWARD:
231
if (state == RepositoryState.MERGING_RESOLVED)
233
// Commit was successful. Now delete the files
234
// used for merge commits
235
repo.WriteMergeCommitMsg(null);
236
repo.WriteMergeHeads(null);
241
case RefUpdate.Result.REJECTED:
242
case RefUpdate.Result.LOCK_FAILURE:
244
throw new ConcurrentRefUpdateException(JGitText.Get().couldNotLockHEAD, ru.GetRef
250
throw new JGitInternalException(MessageFormat.Format(JGitText.Get().updatingRefFailed
251
, Constants.HEAD, commitId.ToString(), rc));
270
catch (UnmergedPathException e)
272
// since UnmergedPathException is a subclass of IOException
273
// which should not be wrapped by a JGitInternalException we
274
// have to catch and re-throw it here
277
catch (IOException e)
279
throw new JGitInternalException(JGitText.Get().exceptionCaughtDuringExecutionOfCommitCommand
284
/// <exception cref="System.IO.IOException"></exception>
285
private void InsertChangeId(ObjectId treeId)
287
ObjectId firstParentId = null;
288
if (!parents.IsEmpty())
290
firstParentId = parents[0];
292
ObjectId changeId = ChangeIdUtil.ComputeChangeId(treeId, firstParentId, author, committer
294
message = ChangeIdUtil.InsertId(message, changeId);
295
if (changeId != null)
297
message = message.ReplaceAll("\nChange-Id: I" + ObjectId.ZeroId.GetName() + "\n",
298
"\nChange-Id: I" + changeId.GetName() + "\n");
302
/// <exception cref="System.IO.IOException"></exception>
303
private DirCache CreateTemporaryIndex(ObjectId headId, DirCache index)
305
ObjectInserter inserter = null;
306
// get DirCacheEditor to modify the index if required
307
DirCacheEditor dcEditor = index.Editor();
308
// get DirCacheBuilder for newly created in-core index to build a
309
// temporary index for this commit
310
DirCache inCoreIndex = DirCache.NewInCore();
311
DirCacheBuilder dcBuilder = inCoreIndex.Builder();
312
onlyProcessed = new bool[only.Count];
313
bool emptyCommit = true;
314
TreeWalk treeWalk = new TreeWalk(repo);
315
int dcIdx = treeWalk.AddTree(new DirCacheIterator(index));
316
int fIdx = treeWalk.AddTree(new FileTreeIterator(repo));
320
hIdx = treeWalk.AddTree(new RevWalk(repo).ParseTree(headId));
322
treeWalk.Recursive = true;
323
while (treeWalk.Next())
325
string path = treeWalk.PathString;
326
// check if current entry's path matches a specified path
327
int pos = LookupOnly(path);
328
CanonicalTreeParser hTree = null;
331
hTree = treeWalk.GetTree<CanonicalTreeParser>(hIdx);
335
// include entry in commit
336
DirCacheIterator dcTree = treeWalk.GetTree<DirCacheIterator>(dcIdx);
337
FileTreeIterator fTree = treeWalk.GetTree<FileTreeIterator>(fIdx);
338
// check if entry refers to a tracked file
339
bool tracked = dcTree != null || hTree != null;
346
// create a new DirCacheEntry with data retrieved from disk
347
DirCacheEntry dcEntry = new DirCacheEntry(path);
348
long entryLength = fTree.GetEntryLength();
349
dcEntry.SetLength(entryLength);
350
dcEntry.LastModified = fTree.GetEntryLastModified();
351
dcEntry.FileMode = fTree.EntryFileMode;
352
bool objectExists = (dcTree != null && fTree.IdEqual(dcTree)) || (hTree != null &&
353
fTree.IdEqual(hTree));
356
dcEntry.SetObjectId(fTree.EntryObjectId);
361
if (inserter == null)
363
inserter = repo.NewObjectInserter();
365
InputStream inputStream = fTree.OpenEntryStream();
368
dcEntry.SetObjectId(inserter.Insert(Constants.OBJ_BLOB, entryLength, inputStream)
377
dcEditor.Add(new _PathEdit_356(dcEntry, path));
378
// add to temporary in-core index
379
dcBuilder.Add(dcEntry);
380
if (emptyCommit && (hTree == null || !hTree.IdEqual(fTree)))
388
// if no file exists on disk, remove entry from index and
389
// don't add it to temporary in-core index
390
dcEditor.Add(new DirCacheEditor.DeletePath(path));
391
if (emptyCommit && hTree != null)
397
// keep track of processed path
398
onlyProcessed[pos] = true;
402
// add entries from HEAD for all other paths
405
// create a new DirCacheEntry with data retrieved from HEAD
406
DirCacheEntry dcEntry = new DirCacheEntry(path);
407
dcEntry.SetObjectId(hTree.EntryObjectId);
408
dcEntry.FileMode = hTree.EntryFileMode;
409
// add to temporary in-core index
410
dcBuilder.Add(dcEntry);
414
// there must be no unprocessed paths left at this point; otherwise an
415
// untracked or unknown path has been specified
416
for (int i = 0; i < onlyProcessed.Length; i++)
418
if (!onlyProcessed[i])
420
throw new JGitInternalException(MessageFormat.Format(JGitText.Get().entryNotFoundByPath
424
// there must be at least one change
427
throw new JGitInternalException(JGitText.Get().emptyCommit);
431
// finish temporary in-core index used for this commit
436
private sealed class _PathEdit_356 : DirCacheEditor.PathEdit
438
public _PathEdit_356(DirCacheEntry dcEntry, string baseArg1) : base(baseArg1)
440
this.dcEntry = dcEntry;
443
public override void Apply(DirCacheEntry ent)
445
ent.CopyMetaData(dcEntry);
448
private readonly DirCacheEntry dcEntry;
452
/// Look an entry's path up in the list of paths specified by the --only/ -o
454
/// In case the complete (file) path (e.g.
457
/// Look an entry's path up in the list of paths specified by the --only/ -o
459
/// In case the complete (file) path (e.g. "d1/d2/f1") cannot be found in
460
/// <code>only</code>, lookup is also tried with (parent) directory paths
461
/// (e.g. "d1/d2" and "d1").
463
/// <param name="pathString">entry's path</param>
464
/// <returns>the item's index in <code>only</code>; -1 if no item matches</returns>
465
private int LookupOnly(string pathString)
468
foreach (string o in only)
470
string p = pathString;
477
int l = p.LastIndexOf("/");
482
p = Sharpen.Runtime.Substring(p, 0, l);
489
/// <summary>Sets default values for not explicitly specified options.</summary>
491
/// Sets default values for not explicitly specified options. Then validates
492
/// that all required data has been provided.
494
/// <param name="state">the state of the repository we are working on</param>
495
/// <exception cref="NGit.Api.Errors.NoMessageException">if the commit message has not been specified
497
private void ProcessOptions(RepositoryState state)
499
if (committer == null)
501
committer = new PersonIdent(repo);
507
// when doing a merge commit parse MERGE_HEAD and MERGE_MSG files
508
if (state == RepositoryState.MERGING_RESOLVED)
512
parents = repo.ReadMergeHeads();
514
catch (IOException e)
516
throw new JGitInternalException(MessageFormat.Format(JGitText.Get().exceptionOccurredDuringReadingOfGIT_DIR
517
, Constants.MERGE_HEAD, e), e);
523
message = repo.ReadMergeCommitMsg();
525
catch (IOException e)
527
throw new JGitInternalException(MessageFormat.Format(JGitText.Get().exceptionOccurredDuringReadingOfGIT_DIR
528
, Constants.MERGE_MSG, e), e);
534
// as long as we don't suppport -C option we have to have
535
// an explicit message
536
throw new NoMessageException(JGitText.Get().commitMessageNotSpecified);
540
/// <param name="message">
541
/// the commit message used for the
542
/// <code>commit</code>
546
/// <code>this</code>
548
public virtual NGit.Api.CommitCommand SetMessage(string message)
551
this.message = message;
555
/// <returns>the commit message used for the <code>commit</code></returns>
556
public virtual string GetMessage()
562
/// Sets the committer for this
563
/// <code>commit</code>
564
/// . If no committer is explicitly
565
/// specified because this method is never called or called with
566
/// <code>null</code>
567
/// value then the committer will be deduced from config info in repository,
568
/// with current time.
570
/// <param name="committer">
571
/// the committer used for the
572
/// <code>commit</code>
576
/// <code>this</code>
578
public virtual NGit.Api.CommitCommand SetCommitter(PersonIdent committer)
581
this.committer = committer;
586
/// Sets the committer for this
587
/// <code>commit</code>
588
/// . If no committer is explicitly
589
/// specified because this method is never called or called with
590
/// <code>null</code>
591
/// value then the committer will be deduced from config info in repository,
592
/// with current time.
594
/// <param name="name">
595
/// the name of the committer used for the
596
/// <code>commit</code>
598
/// <param name="email">
599
/// the email of the committer used for the
600
/// <code>commit</code>
604
/// <code>this</code>
606
public virtual NGit.Api.CommitCommand SetCommitter(string name, string email)
609
return SetCommitter(new PersonIdent(name, email));
613
/// the committer used for the
614
/// <code>commit</code>
615
/// . If no committer was
617
/// <code>null</code>
618
/// is returned and the default
619
/// <see cref="NGit.PersonIdent">NGit.PersonIdent</see>
620
/// of this repo is used during execution of the
623
public virtual PersonIdent GetCommitter()
629
/// Sets the author for this
630
/// <code>commit</code>
631
/// . If no author is explicitly
632
/// specified because this method is never called or called with
633
/// <code>null</code>
634
/// value then the author will be set to the committer.
636
/// <param name="author">
637
/// the author used for the
638
/// <code>commit</code>
642
/// <code>this</code>
644
public virtual NGit.Api.CommitCommand SetAuthor(PersonIdent author)
647
this.author = author;
652
/// Sets the author for this
653
/// <code>commit</code>
654
/// . If no author is explicitly
655
/// specified because this method is never called or called with
656
/// <code>null</code>
657
/// value then the author will be set to the committer.
659
/// <param name="name">
660
/// the name of the author used for the
661
/// <code>commit</code>
663
/// <param name="email">
664
/// the email of the author used for the
665
/// <code>commit</code>
669
/// <code>this</code>
671
public virtual NGit.Api.CommitCommand SetAuthor(string name, string email)
674
return SetAuthor(new PersonIdent(name, email));
678
/// the author used for the
679
/// <code>commit</code>
680
/// . If no author was
682
/// <code>null</code>
683
/// is returned and the default
684
/// <see cref="NGit.PersonIdent">NGit.PersonIdent</see>
685
/// of this repo is used during execution of the
688
public virtual PersonIdent GetAuthor()
694
/// If set to true the Commit command automatically stages files that have
695
/// been modified and deleted, but new files not known by the repository are
699
/// If set to true the Commit command automatically stages files that have
700
/// been modified and deleted, but new files not known by the repository are
701
/// not affected. This corresponds to the parameter -a on the command line.
703
/// <param name="all"></param>
706
/// <code>this</code>
708
/// <exception cref="NGit.Api.Errors.JGitInternalException">in case of an illegal combination of arguments/ options
710
public virtual NGit.Api.CommitCommand SetAll(bool all)
715
throw new JGitInternalException(MessageFormat.Format(JGitText.Get().illegalCombinationOfArguments
716
, "--all", "--only"));
722
/// <summary>Used to amend the tip of the current branch.</summary>
724
/// Used to amend the tip of the current branch. If set to true, the previous
725
/// commit will be amended. This is equivalent to --amend on the command
728
/// <param name="amend"></param>
731
/// <code>this</code>
733
public virtual NGit.Api.CommitCommand SetAmend(bool amend)
741
/// Commit dedicated path only
742
/// This method can be called several times to add multiple paths.
745
/// Commit dedicated path only
746
/// This method can be called several times to add multiple paths. Full file
747
/// paths are supported as well as directory paths; in the latter case this
748
/// commits all files/ directories below the specified path.
750
/// <param name="only">path to commit</param>
753
/// <code>this</code>
755
public virtual NGit.Api.CommitCommand SetOnly(string only)
760
throw new JGitInternalException(MessageFormat.Format(JGitText.Get().illegalCombinationOfArguments
761
, "--only", "--all"));
763
string o = only.EndsWith("/") ? Sharpen.Runtime.Substring(only, 0, only.Length -
766
if (!this.only.Contains(o))
768
this.only.AddItem(o);
774
/// If set to true a change id will be inserted into the commit message
775
/// An existing change id is not replaced.
778
/// If set to true a change id will be inserted into the commit message
779
/// An existing change id is not replaced. An initial change id (I000...)
780
/// will be replaced by the change id.
782
/// <param name="insertChangeId"></param>
785
/// <code>this</code>
787
public virtual NGit.Api.CommitCommand SetInsertChangeId(bool insertChangeId)
790
this.insertChangeId = insertChangeId;