diff --git a/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF index 0c1e3220b..0e8ebc6fa 100644 --- a/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF @@ -12,6 +12,7 @@ Import-Package: org.eclipse.jgit.api;version="[2.1.0,2.2.0)", org.eclipse.jgit.lib;version="[2.1.0,2.2.0)", org.eclipse.jgit.pgm;version="[2.1.0,2.2.0)", org.eclipse.jgit.pgm.opt;version="[2.1.0,2.2.0)", + org.eclipse.jgit.revwalk;version="[2.1.0,2.2.0)", org.eclipse.jgit.storage.file;version="[2.1.0,2.2.0)", org.eclipse.jgit.util;version="[2.1.0,2.2.0)", org.hamcrest.core;bundle-version="[1.1.0,2.0.0)", diff --git a/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/lib/CLIRepositoryTestCase.java b/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/lib/CLIRepositoryTestCase.java index 7d0acd76c..24c40293e 100644 --- a/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/lib/CLIRepositoryTestCase.java +++ b/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/lib/CLIRepositoryTestCase.java @@ -79,4 +79,8 @@ public class CLIRepositoryTestCase extends LocalDiskRepositoryTestCase { throws IOException { return JGitTestUtil.writeTrashFile(db, name, data); } + + protected void deleteTrashFile(final String name) throws IOException { + JGitTestUtil.deleteTrashFile(db, name); + } } diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/StatusTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/StatusTest.java new file mode 100644 index 000000000..ed2c5d037 --- /dev/null +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/StatusTest.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2012, François Rey + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.pgm; + +import static org.junit.Assert.assertEquals; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.lib.CLIRepositoryTestCase; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Test; + +public class StatusTest extends CLIRepositoryTestCase { + @Test + public void testStatus() throws Exception { + Git git = new Git(db); + // Write all files + writeTrashFile("tracked", "tracked"); + writeTrashFile("stagedNew", "stagedNew"); + writeTrashFile("stagedModified", "stagedModified"); + writeTrashFile("stagedDeleted", "stagedDeleted"); + writeTrashFile("trackedModified", "trackedModified"); + writeTrashFile("trackedDeleted", "trackedDeleted"); + writeTrashFile("untracked", "untracked"); + // Test untracked + assertArrayOfLinesEquals(new String[] { // git status output + "# On branch master", // + "# Untracked files:", // + "# ",// + "# \tstagedDeleted", // + "# \tstagedModified", // + "# \tstagedNew", // + "# \ttracked", // + "# \ttrackedDeleted", // + "# \ttrackedModified", // + "# \tuntracked", // + "" // + }, execute("git status")); // + // Add to index + git.add().addFilepattern("tracked").call(); + git.add().addFilepattern("stagedModified").call(); + git.add().addFilepattern("stagedDeleted").call(); + git.add().addFilepattern("trackedModified").call(); + git.add().addFilepattern("trackedDeleted").call(); + // Test staged count + assertArrayOfLinesEquals(new String[] { // git status output + "# On branch master", // + "# Changes to be committed:", // + "# ", // + "# \tnew file: stagedDeleted", // + "# \tnew file: stagedModified", // + "# \tnew file: tracked", // + "# \tnew file: trackedDeleted", // + "# \tnew file: trackedModified", // + "# ", // + "# Untracked files:", // + "# ", // + "# \tstagedNew", // + "# \tuntracked", // + "" // + }, execute("git status")); // + // Commit + git.commit().setMessage("initial commit") + .call(); + assertArrayOfLinesEquals(new String[] { // git status output + "# On branch master", // + "# Untracked files:", // + "# ", // + "# \tstagedNew", // + "# \tuntracked", // + "" // + }, execute("git status")); // + // Make some changes and stage them + writeTrashFile("stagedModified", "stagedModified modified"); + deleteTrashFile("stagedDeleted"); + writeTrashFile("trackedModified", "trackedModified modified"); + deleteTrashFile("trackedDeleted"); + git.add().addFilepattern("stagedModified").call(); + git.rm().addFilepattern("stagedDeleted").call(); + git.add().addFilepattern("stagedNew").call(); + // Test staged/not-staged status + assertArrayOfLinesEquals(new String[] { // git status output + "# On branch master", // + "# Changes to be committed:", // + "# ", // + "# \tdeleted: stagedDeleted", // + "# \tmodified: stagedModified", // + "# \tnew file: stagedNew", // + "# ", // + "# Changes not staged for commit:", // + "# ", // + "# \tdeleted: trackedDeleted", // + "# \tmodified: trackedModified", // + "# ", // + "# Untracked files:", // + "# ", // + "# \tuntracked", // + "" // + }, execute("git status")); // + // Create unmerged file + writeTrashFile("unmerged", "unmerged"); + git.add().addFilepattern("unmerged").call(); + // Commit pending changes + git.add().addFilepattern("trackedModified").call(); + git.rm().addFilepattern("trackedDeleted").call(); + git.commit().setMessage("commit before branching").call(); + assertArrayOfLinesEquals(new String[] { // git status output + "# On branch master", // + "# Untracked files:", // + "# ", // + "# \tuntracked", // + "" // + }, execute("git status")); // + // Checkout new branch + git.checkout().setCreateBranch(true).setName("test").call(); + // Test branch status + assertArrayOfLinesEquals(new String[] { // git status output + "# On branch test", // + "# Untracked files:", // + "# ", // + "# \tuntracked", // + "" // + }, execute("git status")); // + // Commit change and checkout master again + writeTrashFile("unmerged", "changed in test branch"); + git.add().addFilepattern("unmerged").call(); + RevCommit testBranch = git.commit() + .setMessage("changed unmerged in test branch").call(); + assertArrayOfLinesEquals(new String[] { // git status output + "# On branch test", // + "# Untracked files:", // + "# ", // + "# \tuntracked", // + "" // + }, execute("git status")); // + git.checkout().setName("master").call(); + // Change the same file and commit + writeTrashFile("unmerged", "changed in master branch"); + git.add().addFilepattern("unmerged").call(); + git.commit().setMessage("changed unmerged in master branch").call(); + assertArrayOfLinesEquals(new String[] { // git status output + "# On branch master", // + "# Untracked files:", // + "# ", // + "# \tuntracked", // + "" // + }, execute("git status")); // + // Merge test branch into master + git.merge().include(testBranch.getId()).call(); + // Test unmerged status + assertArrayOfLinesEquals(new String[] { // git status output + "# On branch master", // + "# Unmerged paths:", // + "# ", // + "# \tunmerged", // + "# ", // + "# Untracked files:", // + "# ", // + "# \tuntracked", // + "" // + }, execute("git status")); // + // Test detached head + String commitId = db.getRef(Constants.MASTER).getObjectId().name(); + git.checkout().setName(commitId).call(); + assertArrayOfLinesEquals(new String[] { // git status output + "# Not currently on any branch.", // + "# Unmerged paths:", // + "# ", // + "# \tunmerged", // + "# ", // + "# Untracked files:", // + "# ", // + "# \tuntracked", // + "" // + }, execute("git status")); // + } + + private void assertArrayOfLinesEquals(String[] expected, String[] actual) { + assertEquals(toText(expected), toText(actual)); + } + + private String toText(String[] lines) { + StringBuilder b = new StringBuilder(); + for (String s : lines) { + b.append(s); + b.append('\n'); + } + return b.toString(); + } +} diff --git a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin index d8659c05a..488d48af0 100644 --- a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin +++ b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin @@ -25,6 +25,7 @@ org.eclipse.jgit.pgm.RevParse org.eclipse.jgit.pgm.Rm org.eclipse.jgit.pgm.Show org.eclipse.jgit.pgm.ShowRef +org.eclipse.jgit.pgm.Status org.eclipse.jgit.pgm.Tag org.eclipse.jgit.pgm.UploadPack org.eclipse.jgit.pgm.Version diff --git a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/CLIText.properties b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/CLIText.properties index 824ee80b2..0291f5ce2 100644 --- a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/CLIText.properties +++ b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/CLIText.properties @@ -34,6 +34,8 @@ cannotUseObjectsWithGlog=Cannot use --objects with glog cannotWrite=Cannot write {0} cantFindGitDirectory=error: can't find git directory cantWrite=Can't write {0} +changesNotStagedForCommit=Changes not staged for commit: +changesToBeCommitted=Changes to be committed: commitLabel=commit configFileNotFound=configuration file {0} not found conflictingUsageOf_git_dir_andArguments=conflicting usage of --git-dir and arguments @@ -58,6 +60,7 @@ fromURI=From {0} initializedEmptyGitRepositoryIn=Initialized empty Git repository in {0} invalidHttpProxyOnlyHttpSupported=Invalid http_proxy: {0}: Only http supported. jgitVersion=jgit version {0} +lineFormat=# {0} listeningOn=Listening on {0} mergeConflict=CONFLICT(content): Merge conflict in {0} mergeFailed=Automatic merge failed; fix conflicts and then commit the result @@ -123,6 +126,8 @@ notAnIndexFile={0} is not an index file notAnObject={0} is not an object notFound=!! NOT FOUND !! noteObjectTooLargeToPrint=Note object {0} too large to print +notOnAnyBranch=Not currently on any branch. +onBranch=On branch {0} onBranchToBeBorn=You are on a branch yet to be born onlyOneMetaVarExpectedIn=Only one {0} expected in {1}. onlyOneOfIncludeOnlyAllInteractiveCanBeUsed=Only one of --include/--only/--all/--interactive can be used. @@ -136,6 +141,11 @@ remoteSideDoesNotSupportDeletingRefs=remote side does not support deleting refs repaint=Repaint serviceNotSupported=Service '{0}' not supported skippingObject=skipping {0} {1} +statusFileListFormat=\t%1$s +statusFileListFormatWithPrefix=\t%1$-11s %2$s +statusModified=modified: +statusNewFile=new file: +statusRemoved=deleted: switchedToNewBranch=Switched to a new branch ''{0}'' switchedToBranch=Switched to branch ''{0}'' tagLabel=tag @@ -143,7 +153,9 @@ taggerInfo=Tagger: {0} <{1}> timeInMilliSeconds={0} ms tooManyRefsGiven=Too many refs given unknownMergeStrategy=unknown merge strategy {0} specified +unmergedPaths=Unmerged paths: unsupportedOperation=Unsupported operation: {0} +untrackedFiles=Untracked files: usage_Blame=Show what revision and author last modified each line usage_CommandLineClientForamazonsS3Service=Command line client for Amazon's S3 service usage_CommitAll=commit all modified and deleted files @@ -165,6 +177,7 @@ usage_RepositoryToReceiveInto=Repository to receive into usage_ServerSideBackendForJgitFetch=Server side backend for 'jgit fetch' usage_ServerSideBackendForJgitPush=Server side backend for 'jgit push' usage_ShowDiffs=Show diffs +usage_Status=Show the working tree status usage_StopTrackingAFile=Stop tracking a file usage_UpdateRemoteRepositoryFromLocalRefs=Update remote repository from local refs usage_abbrevCommits=abbreviate commits to N + 1 digits diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/CLIText.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/CLIText.java index d748d504b..85b91ab94 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/CLIText.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/CLIText.java @@ -43,6 +43,8 @@ package org.eclipse.jgit.pgm; +import java.text.MessageFormat; + import org.eclipse.jgit.nls.NLS; import org.eclipse.jgit.nls.TranslationBundle; @@ -58,6 +60,18 @@ public class CLIText extends TranslationBundle { return NLS.getBundleFor(CLIText.class); } + /** + * Format the given line for using the format defined by {@link #lineFormat} + * ("# " by default). + * + * @param line + * the line to format + * @return the formatted line + */ + public static String formatLine(String line) { + return MessageFormat.format(get().lineFormat, line); + } + /***/ public String IPZillaPasswordPrompt; /***/ public String alreadyOnBranch; /***/ public String authorInfo; @@ -88,6 +102,8 @@ public class CLIText extends TranslationBundle { /***/ public String cannotWrite; /***/ public String cantFindGitDirectory; /***/ public String cantWrite; + /***/ public String changesNotStagedForCommit; + /***/ public String changesToBeCommitted; /***/ public String commitLabel; /***/ public String conflictingUsageOf_git_dir_andArguments; /***/ public String couldNotCreateBranch; @@ -111,6 +127,7 @@ public class CLIText extends TranslationBundle { /***/ public String initializedEmptyGitRepositoryIn; /***/ public String invalidHttpProxyOnlyHttpSupported; /***/ public String jgitVersion; + /***/ public String lineFormat; /***/ public String listeningOn; /***/ public String mergeConflict; /***/ public String mergeFailed; @@ -169,8 +186,10 @@ public class CLIText extends TranslationBundle { /***/ public String notAnIndexFile; /***/ public String notAnObject; /***/ public String notFound; + /***/ public String notOnAnyBranch; /***/ public String noteObjectTooLargeToPrint; /***/ public String onBranchToBeBorn; + /***/ public String onBranch; /***/ public String onlyOneMetaVarExpectedIn; /***/ public String onlyOneOfIncludeOnlyAllInteractiveCanBeUsed; /***/ public String pathspecDidNotMatch; @@ -183,13 +202,24 @@ public class CLIText extends TranslationBundle { /***/ public String repaint; /***/ public String serviceNotSupported; /***/ public String skippingObject; - /***/ public String switchedToNewBranch; - /***/ public String switchedToBranch; + /***/ public String statusFileListFormat; + /***/ public String statusFileListFormatWithPrefix; + /***/ public String statusModified; + /***/ public String statusNewFile; + /***/ public String statusRemoved; + + /***/ + public String switchedToNewBranch; + + /***/ + public String switchedToBranch; /***/ public String tagLabel; /***/ public String taggerInfo; /***/ public String timeInMilliSeconds; /***/ public String tooManyRefsGiven; /***/ public String unknownMergeStrategy; + /***/ public String unmergedPaths; /***/ public String unsupportedOperation; + /***/ public String untrackedFiles; /***/ public String warningNoCommitGivenOnCommandLine; } diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Status.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Status.java new file mode 100644 index 000000000..64e38599b --- /dev/null +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Status.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2011, François Rey + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.pgm; + +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; + +@Command(usage = "usage_Status", common = true) +class Status extends TextBuiltin { + + protected final String lineFormat = CLIText.get().lineFormat; + + protected final String statusFileListFormat = CLIText.get().statusFileListFormat; + + protected final String statusFileListFormatWithPrefix = CLIText.get().statusFileListFormatWithPrefix; + + @Override + protected void run() throws Exception { + // Print current branch name + final Ref head = db.getRef(Constants.HEAD); + boolean firstHeader = true; + if (head != null && head.isSymbolic()) { + String branch = Repository.shortenRefName(head.getLeaf().getName()); + out.println(CLIText.formatLine( + MessageFormat.format(CLIText.get().onBranch, branch))); + } else + out.println(CLIText.formatLine(CLIText.get().notOnAnyBranch)); + // List changes + org.eclipse.jgit.api.Status status = new Git(db).status().call(); + Collection added = status.getAdded(); + Collection changed = status.getChanged(); + Collection removed = status.getRemoved(); + Collection modified = status.getModified(); + Collection missing = status.getMissing(); + Collection untracked = status.getUntracked(); + Collection unmerged = status.getConflicting(); + Collection toBeCommitted = new ArrayList(added); + toBeCommitted.addAll(changed); + toBeCommitted.addAll(removed); + int nbToBeCommitted = toBeCommitted.size(); + if (nbToBeCommitted > 0) { + printSectionHeader(CLIText.get().changesToBeCommitted); + printList(CLIText.get().statusNewFile, + CLIText.get().statusModified, CLIText.get().statusRemoved, + toBeCommitted, added, changed, removed); + firstHeader = false; + } + Collection notStagedForCommit = new ArrayList(modified); + notStagedForCommit.addAll(missing); + int nbNotStagedForCommit = notStagedForCommit.size(); + if (nbNotStagedForCommit > 0) { + if (!firstHeader) + printSectionHeader(""); + printSectionHeader(CLIText.get().changesNotStagedForCommit); + printList(CLIText.get().statusModified, + CLIText.get().statusRemoved, null, notStagedForCommit, + modified, missing, null); + firstHeader = false; + } + int nbUnmerged = unmerged.size(); + if (nbUnmerged > 0) { + if (!firstHeader) + printSectionHeader(""); + printSectionHeader(CLIText.get().unmergedPaths); + printList(unmerged); + firstHeader = false; + } + int nbUntracked = untracked.size(); + if (nbUntracked > 0) { + if (!firstHeader) + printSectionHeader(""); + printSectionHeader(CLIText.get().untrackedFiles); + printList(untracked); + } + } + + protected void printSectionHeader(String pattern, Object... arguments) { + out.println(CLIText.formatLine(MessageFormat.format(pattern, arguments))); + if (!pattern.equals("")) + out.println(CLIText.formatLine("")); + out.flush(); + } + + protected int printList(Collection list) { + if (!list.isEmpty()) { + List sortedList = new ArrayList(list); + java.util.Collections.sort(sortedList); + for (String filename : sortedList) { + out.println(CLIText.formatLine(String.format( + statusFileListFormat, filename))); + } + out.flush(); + return list.size(); + } else + return 0; + } + + protected int printList(String status1, String status2, String status3, + Collection list, Collection set1, + Collection set2, + @SuppressWarnings("unused") Collection set3) { + List sortedList = new ArrayList(list); + java.util.Collections.sort(sortedList); + for (String filename : sortedList) { + String prefix; + if (set1.contains(filename)) + prefix = status1; + else if (set2.contains(filename)) + prefix = status2; + else + // if (set3.contains(filename)) + prefix = status3; + out.println(CLIText.formatLine(String.format( + statusFileListFormatWithPrefix, prefix, filename))); + out.flush(); + } + return list.size(); + } +}