2 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
4 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
6 * The contents of this file are subject to the terms of either the GNU
7 * General Public License Version 2 only ("GPL") or the Common
8 * Development and Distribution License("CDDL") (collectively, the
9 * "License"). You may not use this file except in compliance with the
10 * License. You can obtain a copy of the License at
11 * http://www.netbeans.org/cddl-gplv2.html
12 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
13 * specific language governing permissions and limitations under the
14 * License. When distributing the software, include this License Header
15 * Notice in each file and include the License file at
16 * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
17 * particular file as subject to the "Classpath" exception as provided
18 * by Sun in the GPL Version 2 section of the License file that
19 * accompanied this code. If applicable, add the following below the
20 * License Header, with the fields enclosed by brackets [] replaced by
21 * your own identifying information:
22 * "Portions Copyrighted [year] [name of copyright owner]"
26 * The Original Software is NetBeans. The Initial Developer of the Original
27 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
28 * Microsystems, Inc. All Rights Reserved.
29 * Portions Copyright 2008 Alexander Coles (Ikonoklastik Productions).
31 * If you wish your version of this file to be governed by only the CDDL
32 * or only the GPL Version 2, indicate your decision by adding
33 * "[Contributor] elects to include this software in this distribution
34 * under the [CDDL or GPL Version 2] license." If you do not indicate a
35 * single choice of license, a recipient has the option to distribute
36 * your version of this file under either the CDDL, the GPL Version 2 or
37 * to extend the choice of license to its licensees as provided above.
38 * However, if you add GPL Version 2 code and therefore, elected the GPL
39 * Version 2 license, then the option applies only if the new code is
40 * made subject to such option by the copyright holder.
42 package org
.nbgit
.ui
.commit
;
44 import java
.awt
.BorderLayout
;
45 import java
.awt
.Dialog
;
46 import java
.awt
.event
.ActionEvent
;
48 import java
.text
.MessageFormat
;
49 import java
.util
.ArrayList
;
50 import java
.util
.HashSet
;
51 import java
.util
.List
;
53 import java
.util
.Properties
;
54 import java
.util
.ResourceBundle
;
56 import javax
.swing
.JButton
;
57 import javax
.swing
.JComponent
;
58 import javax
.swing
.JOptionPane
;
59 import javax
.swing
.event
.TableModelEvent
;
60 import javax
.swing
.event
.TableModelListener
;
61 import org
.nbgit
.StatusInfo
;
62 import org
.nbgit
.StatusCache
;
64 import org
.nbgit
.GitModuleConfig
;
65 import org
.nbgit
.GitProgressSupport
;
66 import org
.nbgit
.OutputLogger
;
67 import org
.nbgit
.client
.CommitBuilder
;
68 import org
.nbgit
.ui
.GitFileNode
;
69 import org
.nbgit
.ui
.ContextAction
;
70 import org
.nbgit
.util
.GitProjectUtils
;
71 import org
.nbgit
.util
.GitUtils
;
72 import org
.netbeans
.modules
.versioning
.spi
.VCSContext
;
73 import org
.netbeans
.modules
.versioning
.util
.DialogBoundsPreserver
;
74 import org
.netbeans
.modules
.versioning
.util
.Utils
;
75 import org
.netbeans
.modules
.versioning
.util
.VersioningEvent
;
76 import org
.netbeans
.modules
.versioning
.util
.VersioningListener
;
77 import org
.openide
.DialogDescriptor
;
78 import org
.openide
.DialogDisplayer
;
79 import org
.openide
.util
.HelpCtx
;
80 import org
.openide
.util
.NbBundle
;
81 import org
.openide
.util
.RequestProcessor
;
84 * Commit action for Git:
85 * git commit - commit the specified files or all outstanding changes
89 public class CommitAction
extends ContextAction
{
91 static final String RECENT_COMMIT_MESSAGES
= "recentCommitMessage"; // NOI18N
92 static final String SIGN_OFF_MESSAGE
= "signOffMessage"; // NOI18N
94 public CommitAction(String name
, VCSContext context
) {
99 public boolean isEnabled() {
100 StatusCache cache
= Git
.getInstance().getStatusCache();
101 return cache
.containsFileOfStatus(context
, StatusInfo
.STATUS_LOCAL_CHANGE
);
104 public void performAction(ActionEvent e
) {
105 final File root
= GitUtils
.getRootFile(context
);
107 OutputLogger logger
= OutputLogger
.getLogger(Git
.GIT_OUTPUT_TAB_TITLE
);
108 logger
.outputInRed(NbBundle
.getMessage(CommitAction
.class, "MSG_COMMIT_TITLE")); // NOI18N
109 logger
.outputInRed(NbBundle
.getMessage(CommitAction
.class, "MSG_COMMIT_TITLE_SEP")); // NOI18N
111 NbBundle
.getMessage(CommitAction
.class, "MSG_COMMIT_NOT_SUPPORTED_INVIEW_INFO")); // NOI18N
112 logger
.output(""); // NOI18N
114 JOptionPane
.showMessageDialog(null,
115 NbBundle
.getMessage(CommitAction
.class, "MSG_COMMIT_NOT_SUPPORTED_INVIEW"),// NOI18N
116 NbBundle
.getMessage(CommitAction
.class, "MSG_COMMIT_NOT_SUPPORTED_INVIEW_TITLE"),// NOI18N
117 JOptionPane
.INFORMATION_MESSAGE
);
120 String contentTitle
= Utils
.getContextDisplayName(context
);
122 commit(contentTitle
, context
);
125 public static void commit(String contentTitle
, final VCSContext ctx
) {
126 StatusCache cache
= Git
.getInstance().getStatusCache();
127 File
[] roots
= ctx
.getRootFiles().toArray(new File
[ctx
.getRootFiles().size()]);
128 if (roots
== null || roots
.length
== 0) {
131 final File repository
= GitUtils
.getRootFile(ctx
);
132 if (repository
== null) {
135 String projName
= GitProjectUtils
.getProjectName(repository
);
136 if (projName
== null) {
137 File projFile
= GitUtils
.getProjectFile(ctx
);
138 projName
= GitProjectUtils
.getProjectName(projFile
);
140 final String prjName
= projName
;
142 File
[][] split
= Utils
.splitFlatOthers(roots
);
143 List
<File
> fileList
= new ArrayList
<File
>();
144 for (int c
= 0; c
< split
.length
; c
++) {
146 boolean recursive
= c
== 1;
148 File
[] files
= cache
.listFiles(ctx
, StatusInfo
.STATUS_LOCAL_CHANGE
);
149 for (int i
= 0; i
< files
.length
; i
++) {
150 for (int r
= 0; r
< roots
.length
; r
++) {
151 if (GitUtils
.isParentOrEqual(roots
[r
], files
[i
])) {
152 if (!fileList
.contains(files
[i
])) {
153 fileList
.add(files
[i
]);
159 File
[] files
= GitUtils
.flatten(roots
, StatusInfo
.STATUS_LOCAL_CHANGE
);
160 for (int i
= 0; i
< files
.length
; i
++) {
161 if (!fileList
.contains(files
[i
])) {
162 fileList
.add(files
[i
]);
168 if (fileList
.size() == 0) {
169 return; // show commit dialog
171 final CommitPanel panel
= new CommitPanel();
172 final CommitTable data
= new CommitTable(panel
.filesLabel
, CommitTable
.COMMIT_COLUMNS
, new String
[]{CommitTableModel
.COLUMN_NAME_PATH
});
174 panel
.setCommitTable(data
);
176 Properties props
= GitModuleConfig
.getDefault().getProperties(repository
);
177 String signOff
= props
.getProperty("nbgit.signoff");
178 if (signOff
.equals("yes")) {
179 signOff
= "Signed-off-by: " +props
.getProperty("user.name")
180 + " <" + props
.getProperty("user.email") + ">";
181 panel
.putClientProperty(SIGN_OFF_MESSAGE
, signOff
);
185 ArrayList
<GitFileNode
> nodesList
= new ArrayList
<GitFileNode
>(fileList
.size());
187 for (File file
: fileList
) {
188 GitFileNode node
= new GitFileNode(file
);
191 nodes
= nodesList
.toArray(new GitFileNode
[fileList
.size()]);
192 data
.setNodes(nodes
);
194 JComponent component
= data
.getComponent();
195 panel
.filesPanel
.setLayout(new BorderLayout());
196 panel
.filesPanel
.add(component
, BorderLayout
.CENTER
);
198 DialogDescriptor dd
= new DialogDescriptor(panel
, org
.openide
.util
.NbBundle
.getMessage(CommitAction
.class, "CTL_CommitDialog_Title", contentTitle
)); // NOI18N
200 final JButton commitButton
= new JButton();
201 org
.openide
.awt
.Mnemonics
.setLocalizedText(commitButton
, org
.openide
.util
.NbBundle
.getMessage(CommitAction
.class, "CTL_Commit_Action_Commit"));
202 commitButton
.getAccessibleContext().setAccessibleName(org
.openide
.util
.NbBundle
.getMessage(CommitAction
.class, "ACSN_Commit_Action_Commit"));
203 commitButton
.getAccessibleContext().setAccessibleDescription(org
.openide
.util
.NbBundle
.getMessage(CommitAction
.class, "ACSD_Commit_Action_Commit"));
204 final JButton cancelButton
= new JButton(org
.openide
.util
.NbBundle
.getMessage(CommitAction
.class, "CTL_Commit_Action_Cancel")); // NOI18N
205 org
.openide
.awt
.Mnemonics
.setLocalizedText(cancelButton
, org
.openide
.util
.NbBundle
.getMessage(CommitAction
.class, "CTL_Commit_Action_Cancel"));
206 cancelButton
.getAccessibleContext().setAccessibleName(org
.openide
.util
.NbBundle
.getMessage(CommitAction
.class, "ACSN_Commit_Action_Cancel"));
207 cancelButton
.getAccessibleContext().setAccessibleDescription(org
.openide
.util
.NbBundle
.getMessage(CommitAction
.class, "ACSD_Commit_Action_Cancel"));
209 commitButton
.setEnabled(false);
210 dd
.setOptions(new Object
[]{commitButton
, cancelButton
});
211 dd
.setHelpCtx(new HelpCtx(CommitAction
.class));
212 panel
.addVersioningListener(new VersioningListener() {
214 public void versioningEvent(VersioningEvent event
) {
215 refreshCommitDialog(panel
, data
, commitButton
);
218 data
.getTableModel().addTableModelListener(new TableModelListener() {
220 public void tableChanged(TableModelEvent e
) {
221 refreshCommitDialog(panel
, data
, commitButton
);
224 commitButton
.setEnabled(containsCommitable(data
));
226 panel
.putClientProperty("contentTitle", contentTitle
); // NOI18N
227 panel
.putClientProperty("DialogDescriptor", dd
); // NOI18N
228 final Dialog dialog
= DialogDisplayer
.getDefault().createDialog(dd
);
230 dialog
.addWindowListener(new DialogBoundsPreserver(GitModuleConfig
.getDefault().getPreferences(), "git.commit.dialog")); // NOI18N
232 dialog
.setVisible(true);
234 if (dd
.getValue() == commitButton
) {
236 final Map
<GitFileNode
, CommitOptions
> commitFiles
= data
.getCommitFiles();
237 String origMessage
= panel
.messageTextArea
.getText();
238 String stripSpace
= props
.getProperty("nbgit.stripspace");
239 if (stripSpace
.equals("yes")) {
240 origMessage
= stripSpace(origMessage
);
242 final String message
= origMessage
;
243 org
.netbeans
.modules
.versioning
.util
.Utils
.insert(GitModuleConfig
.getDefault().getPreferences(), RECENT_COMMIT_MESSAGES
, origMessage
, 20);
244 RequestProcessor rp
= Git
.getInstance().getRequestProcessor(repository
.getAbsolutePath());
245 GitProgressSupport support
= new GitProgressSupport() {
247 public void perform() {
248 OutputLogger logger
= getLogger();
249 performCommit(message
, commitFiles
, ctx
, this, prjName
, logger
);
252 support
.start(rp
, repository
.getAbsolutePath(), org
.openide
.util
.NbBundle
.getMessage(CommitAction
.class, "LBL_Commit_Progress")); // NOI18N
256 private static boolean containsCommitable(CommitTable data
) {
257 Map
<GitFileNode
, CommitOptions
> map
= data
.getCommitFiles();
258 for (CommitOptions co
: map
.values()) {
259 if (co
!= CommitOptions
.EXCLUDE
) {
267 * User changed a commit action.
272 private static void refreshCommitDialog(CommitPanel panel
, CommitTable table
, JButton commit
) {
273 ResourceBundle loc
= NbBundle
.getBundle(CommitAction
.class);
274 Map
<GitFileNode
, CommitOptions
> files
= table
.getCommitFiles();
275 Set
<String
> stickyTags
= new HashSet
<String
>();
276 boolean conflicts
= false;
278 boolean enabled
= commit
.isEnabled();
280 for (GitFileNode fileNode
: files
.keySet()) {
282 CommitOptions options
= files
.get(fileNode
);
283 if (options
== CommitOptions
.EXCLUDE
) {
285 //stickyTags.add(GitUtils.getCopy(fileNode.getFile()));
287 int status
= fileNode
.getInformation().getStatus();
288 if ((status
& StatusInfo
.STATUS_REMOTE_CHANGE
) != 0 || status
== StatusInfo
.STATUS_VERSIONED_CONFLICT
) {
290 String msg
= (status
== StatusInfo
.STATUS_VERSIONED_CONFLICT
) ? loc
.getString("MSG_CommitForm_ErrorConflicts") : // NOI18N
291 loc
.getString("MSG_CommitForm_ErrorRemoteChanges"); // NOI18N
292 panel
.setErrorLabel("<html><font color=\"#002080\">" + msg
+ "</font></html>"); // NOI18N
295 //stickyTags.add(GitUtils.getCopy(fileNode.getFile()));
299 if (stickyTags
.size() > 1) {
300 table
.setColumns(new String
[]{CommitTableModel
.COLUMN_NAME_NAME
, CommitTableModel
.COLUMN_NAME_BRANCH
, CommitTableModel
.COLUMN_NAME_STATUS
,
301 CommitTableModel
.COLUMN_NAME_ACTION
, CommitTableModel
.COLUMN_NAME_PATH
304 table
.setColumns(new String
[]{CommitTableModel
.COLUMN_NAME_NAME
, CommitTableModel
.COLUMN_NAME_STATUS
,
305 CommitTableModel
.COLUMN_NAME_ACTION
, CommitTableModel
.COLUMN_NAME_PATH
308 String contentTitle
= (String
) panel
.getClientProperty("contentTitle"); // NOI18N
310 DialogDescriptor dd
= (DialogDescriptor
) panel
.getClientProperty("DialogDescriptor"); // NOI18N
312 if (stickyTags
.size() <= 1) {
313 String stickyTag
= stickyTags
.size() == 0 ?
null : stickyTags
.iterator().next();
314 if (stickyTag
== null) {
315 dd
.setTitle(MessageFormat
.format(loc
.getString("CTL_CommitDialog_Title"), new Object
[]{contentTitle
})); // NOI18N
316 errorLabel
= ""; // NOI18N
318 dd
.setTitle(MessageFormat
.format(loc
.getString("CTL_CommitDialog_Title_Branch"), new Object
[]{contentTitle
, stickyTag
})); // NOI18N
319 String msg
= MessageFormat
.format(loc
.getString("MSG_CommitForm_InfoBranch"), new Object
[]{stickyTag
}); // NOI18N
320 errorLabel
= "<html><font color=\"#002080\">" + msg
+ "</font></html>"; // NOI18N
323 dd
.setTitle(MessageFormat
.format(loc
.getString("CTL_CommitDialog_Title_Branches"), new Object
[]{contentTitle
})); // NOI18N
324 String msg
= loc
.getString("MSG_CommitForm_ErrorMultipleBranches"); // NOI18N
325 errorLabel
= "<html><font color=\"#CC0000\">" + msg
+ "</font></html>"; // NOI18N
328 panel
.setErrorLabel(errorLabel
);
331 commit
.setEnabled(enabled
&& containsCommitable(table
));
334 private static void performCommit(String message
, Map
<GitFileNode
, CommitOptions
> commitFiles
,
335 VCSContext ctx
, GitProgressSupport support
, String prjName
, OutputLogger logger
) {
336 StatusCache cache
= Git
.getInstance().getStatusCache();
337 final File repository
= GitUtils
.getRootFile(ctx
);
338 List
<File
> addCandidates
= new ArrayList
<File
>();
339 List
<File
> deleteCandidates
= new ArrayList
<File
>();
340 int commitCandidates
= 0;
342 List
<String
> excPaths
= new ArrayList
<String
>();
343 List
<String
> incPaths
= new ArrayList
<String
>();
344 for (GitFileNode node
: commitFiles
.keySet()) {
345 if (support
.isCanceled()) {
348 CommitOptions option
= commitFiles
.get(node
);
349 if (option
!= CommitOptions
.EXCLUDE
) {
350 int status
= cache
.getStatus(node
.getFile()).getStatus();
351 if ((status
& StatusInfo
.STATUS_NOTVERSIONED_NEWLOCALLY
) != 0) {
352 addCandidates
.add(node
.getFile());
353 } else if ((status
& StatusInfo
.STATUS_VERSIONED_DELETEDLOCALLY
) != 0) {
354 deleteCandidates
.add(node
.getFile());
356 addCandidates
.add(node
.getFile());
359 incPaths
.add(node
.getFile().getAbsolutePath());
361 excPaths
.add(node
.getFile().getAbsolutePath());
364 if (support
.isCanceled()) {
367 if (!excPaths
.isEmpty()) {
368 GitModuleConfig
.getDefault().addExclusionPaths(excPaths
);
370 if (!incPaths
.isEmpty()) {
371 GitModuleConfig
.getDefault().removeExclusionPaths(incPaths
);
375 NbBundle
.getMessage(CommitAction
.class,
376 "MSG_COMMIT_TITLE")); // NOI18N
378 NbBundle
.getMessage(CommitAction
.class,
379 "MSG_COMMIT_TITLE_SEP")); // NOI18N
380 logger
.output(message
); // NOI18N
382 CommitBuilder
.create(repository
).
384 addAll(addCandidates
).
385 deleteAll(deleteCandidates
).
389 if (commitCandidates
== 1) {
391 NbBundle
.getMessage(CommitAction
.class,
392 "MSG_COMMIT_INIT_SEP_ONE", commitCandidates
, prjName
));
395 NbBundle
.getMessage(CommitAction
.class,
396 "MSG_COMMIT_INIT_SEP", commitCandidates
, prjName
));
398 } catch (Exception ex
) {
401 cache
.refreshCached(ctx
);
402 logger
.outputInRed(NbBundle
.getMessage(CommitAction
.class, "MSG_COMMIT_DONE")); // NOI18N
403 logger
.output(""); // NOI18N
407 static String
stripSpace(String message
) {
408 StringBuilder builder
= new StringBuilder();
409 String
[] lines
= message
.split("\n");
413 for (String line
: lines
) {
414 int j
= line
.length();
417 if (!Character
.isWhitespace(line
.charAt(j
- 1))) {
424 if (builder
.length() > 0) {
427 builder
.append(line
, 0, j
);
428 builder
.append("\n");
430 } else if (sep
.length() == 0) {
437 return builder
.toString();