diff --git a/app/lib/org.eclipse.jgit-4.3.0.201604071810-r.jar b/app/lib/org.eclipse.jgit-4.3.0.201604071810-r.jar new file mode 100644 index 00000000000..d0a4070b5fb Binary files /dev/null and b/app/lib/org.eclipse.jgit-4.3.0.201604071810-r.jar differ diff --git a/app/lib/slf4j-api-1.7.21.jar b/app/lib/slf4j-api-1.7.21.jar new file mode 100644 index 00000000000..7811fa0a832 Binary files /dev/null and b/app/lib/slf4j-api-1.7.21.jar differ diff --git a/app/lib/slf4j-simple-1.7.21.jar b/app/lib/slf4j-simple-1.7.21.jar new file mode 100644 index 00000000000..da46fee9cbb Binary files /dev/null and b/app/lib/slf4j-simple-1.7.21.jar differ diff --git a/app/src/cc/arduino/view/git/GitDiffForm.form b/app/src/cc/arduino/view/git/GitDiffForm.form new file mode 100644 index 00000000000..b7686ad6d87 --- /dev/null +++ b/app/src/cc/arduino/view/git/GitDiffForm.form @@ -0,0 +1,76 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/app/src/cc/arduino/view/git/GitDiffForm.java b/app/src/cc/arduino/view/git/GitDiffForm.java new file mode 100644 index 00000000000..7c9a100aac5 --- /dev/null +++ b/app/src/cc/arduino/view/git/GitDiffForm.java @@ -0,0 +1,145 @@ +/* + * This file is part of Arduino. + * + * Copyright 2015 Arduino LLC (http://www.arduino.cc/) + * + * Arduino is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + * As a special exception, you may use this file as part of a free software + * library without restriction. Specifically, if other files instantiate + * templates or use macros or inline functions from this file, or you compile + * this file and link it with other files to produce an executable, this + * file does not by itself cause the resulting executable to be covered by + * the GNU General Public License. This exception does not however + * invalidate any other reasons why the executable file might be covered by + * the GNU General Public License. + */ + +package cc.arduino.view.git; + +import processing.app.git.GitOutputParser; + +import javax.swing.text.BadLocationException; +import javax.swing.text.DefaultHighlighter; +import javax.swing.text.Highlighter; + +import java.awt.*; + +import static processing.app.I18n.tr; + +public class GitDiffForm extends javax.swing.JFrame { + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + jScrollPane1 = new javax.swing.JScrollPane(); + oldTextPane = new javax.swing.JTextPane(); + jScrollPane2 = new javax.swing.JScrollPane(); + newTextPane = new javax.swing.JTextPane(); + + setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); + setTitle(tr("Diff")); + setResizable(false); + + jScrollPane1.setViewportView(oldTextPane); + oldTextPane.setEditable(false); + jScrollPane2.setViewportView(newTextPane); + newTextPane.setEditable(false); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(25, 25, 25) + .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 400, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(18, 18, 18) + .addComponent(jScrollPane2, javax.swing.GroupLayout.DEFAULT_SIZE, 395, Short.MAX_VALUE) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 427, Short.MAX_VALUE) + .addComponent(jScrollPane2)) + .addContainerGap()) + ); + + pack(); + }// //GEN-END:initComponents + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JScrollPane jScrollPane1; + private javax.swing.JScrollPane jScrollPane2; + private javax.swing.JTextPane newTextPane; + private javax.swing.JTextPane oldTextPane; + // End of variables declaration//GEN-END:variables + + private Color addedColor = Color.GREEN; + private Color deletedColor = Color.PINK; + + private Highlighter.HighlightPainter addedPainter = new DefaultHighlighter.DefaultHighlightPainter(addedColor); + private Highlighter.HighlightPainter deletedPainter = new DefaultHighlighter.DefaultHighlightPainter(deletedColor); + + public GitDiffForm(GitOutputParser.ParserResult parserResult) { + initComponents(); + + GitOutputParser.DiffText newText = parserResult.getNewText(); + GitOutputParser.DiffText oldText = parserResult.getOldText(); + + newTextPane.setText(newText.getText()); + oldTextPane.setText(oldText.getText()); + + for (int i = 0; i < newText.getBeginIndexes().size(); i++) { + highlightNew( + newText.getBeginIndexes().get(i), + newText.getEndIndexes().get(i) + ); + } + + for (int i = 0; i < oldText.getBeginIndexes().size(); i++) { + highlightOld( + oldText.getBeginIndexes().get(i), + oldText.getEndIndexes().get(i) + ); + } + + } + + public void highlightNew(int begin, int end) { + try { + newTextPane.getHighlighter().addHighlight(begin, end, addedPainter); + } catch (BadLocationException e) { + e.printStackTrace(); + } + } + + public void highlightOld(int begin, int end) { + try { + oldTextPane.getHighlighter().addHighlight(begin, end, deletedPainter); + } catch (BadLocationException e) { + e.printStackTrace(); + } + } + +} diff --git a/app/src/processing/app/Editor.java b/app/src/processing/app/Editor.java index cc7356b4e81..a96f03f3c26 100644 --- a/app/src/processing/app/Editor.java +++ b/app/src/processing/app/Editor.java @@ -39,6 +39,7 @@ import org.fife.ui.rtextarea.RTextScrollPane; import processing.app.debug.RunnerException; import processing.app.forms.PasswordAuthorizationDialog; +import processing.app.git.GitManager; import processing.app.helpers.Keys; import processing.app.helpers.OSUtils; import processing.app.helpers.PreferencesMapException; @@ -160,7 +161,7 @@ public boolean test(Sketch sketch) { static volatile AbstractMonitor serialMonitor; static AbstractMonitor serialPlotter; - + final EditorHeader header; EditorStatus status; EditorConsole console; @@ -202,6 +203,10 @@ public boolean test(Sketch sketch) { private Runnable exportAppHandler; private Runnable timeoutUploadHandler; + private GitManager gitManager = new GitManager(); + + + public Editor(Base ibase, File file, int[] storedLocation, int[] defaultLocation, Platform platform) throws Exception { super("Arduino"); this.base = ibase; @@ -252,7 +257,7 @@ public void windowDeactivated(WindowEvent e) { //PdeKeywords keywords = new PdeKeywords(); //sketchbook = new Sketchbook(this); - + buildMenuBar(); // For rev 0120, placing things inside a JPanel @@ -300,7 +305,7 @@ public void windowDeactivated(WindowEvent e) { scrollPane.setViewportBorder(BorderFactory.createEmptyBorder()); scrollPane.setLineNumbersEnabled(PreferencesData.getBoolean("editor.linenumbers")); scrollPane.setIconRowHeaderEnabled(false); - + Gutter gutter = scrollPane.getGutter(); gutter.setBookmarkingEnabled(false); //gutter.setBookmarkIcon(CompletionsRenderer.getIcon(CompletionType.TEMPLATE)); @@ -572,6 +577,7 @@ public void menuSelected(MenuEvent e) { menubar.add(toolsMenu); menubar.add(buildHelpMenu()); + menubar.add(buildGitMenu()); setJMenuBar(menubar); } @@ -696,6 +702,59 @@ public void actionPerformed(ActionEvent e) { return fileMenu; } + private JMenu buildGitMenu() { + JMenu gitMenu = new JMenu(tr("Git")); + gitMenu.setMnemonic(KeyEvent.VK_G); + + JMenuItem item = newJMenuItem(tr("Init"), 'I'); + item.addActionListener(e -> gitManager + .init(sketch.getFolder()) + ); + gitMenu.add(item); + + item = newJMenuItem(tr("Commit"), 'C'); + item.addActionListener(e -> { + String commitMessage = JOptionPane.showInputDialog("Commit message:"); + gitManager.commit(sketch.getFolder(), commitMessage); + }); + gitMenu.add(item); + + item = newJMenuItem(tr("Log"), 'L'); + item.addActionListener(e -> + gitManager.log(sketch.getFolder()) + ); + gitMenu.add(item); + + item = newJMenuItem(tr("Diff"), 'D'); + item.addActionListener(e -> { + gitManager.diff(sketch.getFolder(), sketch.getCurrentCode().getFile()); + }); + gitMenu.add(item); + + item = newJMenuItem(tr("Reset"), 'R'); + item.addActionListener(e -> { + int i = JOptionPane.showConfirmDialog( + null, + tr("This is revert all changes.\n Are you sure?"), + tr("alert"), + JOptionPane.OK_CANCEL_OPTION + ); + if (i == JOptionPane.OK_OPTION) { + gitManager.reset(sketch.getFolder(), sketch.getCurrentCode().getFile()); + } + // Update the currently visible program with its code + try { + sketch.load(true); + textarea.setText(sketch.getCurrentCode().getProgram()); + } catch (IOException e1) { + e1.printStackTrace(); + } + }); + gitMenu.add(item); + + return gitMenu; + } + public void rebuildRecentSketchesMenu() { recentSketchesMenu.removeAll(); for (JMenuItem recentSketchMenuItem : base.getRecentSketchesMenuItems()) { @@ -1598,7 +1657,7 @@ public void actionPerformed(ActionEvent e) { } protected void updateUndoState() { - + UndoManager undo = textarea.getUndoManager(); if (undo.canUndo()) { @@ -1634,7 +1693,7 @@ public void actionPerformed(ActionEvent e) { protected void updateRedoState() { UndoManager undo = textarea.getUndoManager(); - + if (undo.canRedo()) { redoItem.setEnabled(true); redoItem.setText(undo.getRedoPresentationName()); @@ -1797,7 +1856,7 @@ protected void setCode(final SketchCodeDocument codeDoc) { } // set up this guy's own undo manager // code.undo = new UndoManager(); - + codeDoc.setDocument(document); } @@ -1805,17 +1864,17 @@ protected void setCode(final SketchCodeDocument codeDoc) { codeDoc.setUndo(new LastUndoableEditAwareUndoManager(textarea, this)); document.addUndoableEditListener(codeDoc.getUndo()); } - + // Update the document object that's in use textarea.switchDocument(document, codeDoc.getUndo()); - + // HACK multiple tabs: for update Listeners of Gutter, forcin call: Gutter.setTextArea(RTextArea) // BUG: https://github.com/bobbylight/RSyntaxTextArea/issues/84 scrollPane.setViewportView(textarea); - + textarea.select(codeDoc.getSelectionStart(), codeDoc.getSelectionStop()); textarea.requestFocus(); // get the caret blinking - + final int position = codeDoc.getScrollPosition(); // invokeLater: Expect the document to be rendered correctly to set the new position @@ -2562,7 +2621,7 @@ public void handleSerial() { return; } } - + if (serialMonitor != null) { // The serial monitor already exists @@ -2651,7 +2710,7 @@ public void handleSerial() { } while (serialMonitor.requiresAuthorization() && !success); } - + public void handlePlotter() { if(serialMonitor != null) { if(serialMonitor.isClosed()) { @@ -2661,7 +2720,7 @@ public void handlePlotter() { return; } } - + if (serialPlotter != null) { // The serial plotter already exists @@ -2982,7 +3041,7 @@ private void configurePopupMenu(final SketchTextArea textarea){ item.setName("menuToolsAutoFormat"); menu.add(item); - + item = newJMenuItem(tr("Comment/Uncomment"), '/'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { @@ -3026,7 +3085,7 @@ public void actionPerformed(ActionEvent e) { final JMenuItem referenceItem = new JMenuItem(tr("Find in Reference")); referenceItem.addActionListener(this::handleFindReference); - menu.add(referenceItem); + menu.add(referenceItem); final JMenuItem openURLItem = new JMenuItem(tr("Open URL")); openURLItem.addActionListener(new ActionListener() { @@ -3034,15 +3093,15 @@ public void actionPerformed(ActionEvent e) { Base.openURL(e.getActionCommand()); } }); - menu.add(openURLItem); - + menu.add(openURLItem); + menu.addPopupMenuListener(new PopupMenuListener() { @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { String referenceFile = base.getPdeKeywords().getReference(getCurrentKeyword()); referenceItem.setEnabled(referenceFile != null); - + int offset = textarea.getCaretPosition(); org.fife.ui.rsyntaxtextarea.Token token = RSyntaxUtilities.getTokenAtOffset(textarea, offset); if (token != null && token.isHyperlink()) { @@ -3075,4 +3134,6 @@ public void goToLine(int line) { } } + + } diff --git a/app/src/processing/app/git/GitManager.java b/app/src/processing/app/git/GitManager.java new file mode 100644 index 00000000000..0579593aee0 --- /dev/null +++ b/app/src/processing/app/git/GitManager.java @@ -0,0 +1,198 @@ +package processing.app.git; + +import cc.arduino.view.git.GitDiffForm; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.NoHeadException; +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.diff.DiffFormatter; +import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.eclipse.jgit.treewalk.AbstractTreeIterator; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; +import org.eclipse.jgit.treewalk.FileTreeIterator; +import org.eclipse.jgit.treewalk.filter.PathFilter; + +import java.io.*; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.function.Consumer; + + +public class GitManager { + + public void init(File repoDir) { + try (Git git = Git.init().setDirectory(repoDir).call()) { + System.out.println("Having repository: " + git.getRepository().getDirectory()); + } catch (GitAPIException e) { + e.printStackTrace(); + } + } + + public void reset(File repoDir, File file) { + Consumer resetCommand = git -> { + try { + // We need get relative repository path + git.checkout().addPath( + getRelativePath(repoDir, file) + ).call(); + + System.out.println("Reset file " + file.getName()); + } catch (GitAPIException e) { + e.printStackTrace(); + } + }; + runGitCommand(repoDir, resetCommand); + } + + private String getRelativePath(File repoDir, File file) { + Path pathAbsolute = Paths.get(file.getPath()); + Path pathBase = Paths.get(repoDir.getPath()); + Path pathRelative = pathBase.relativize(pathAbsolute); + return pathRelative.toString(); + } + + public void diff(File repoDir, File file) { + Consumer commitCommand = git -> { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + try (DiffFormatter diffFormatter = new DiffFormatter(outputStream)) { + diffFormatter.setContext(20); + diffFormatter.setRepository(git.getRepository()); + diffFormatter.setPathFilter(PathFilter.create( + getRelativePath(repoDir, file) + )); + + AbstractTreeIterator commitTreeIterator = prepareTreeParser(git.getRepository(), Constants.HEAD); + FileTreeIterator workTreeIterator = new FileTreeIterator(git.getRepository()); + + List diffEntries = diffFormatter.scan(commitTreeIterator, workTreeIterator); + diffFormatter.format(diffEntries); + + GitOutputParser parser = new GitOutputParser(); + GitOutputParser.ParserResult result = parser.diffParser(new ByteArrayInputStream(outputStream.toByteArray())); + + GitDiffForm gitDiffForm = new GitDiffForm(result); + gitDiffForm.setVisible(true); + + } catch (IOException e) { + e.printStackTrace(); + } + }; + runGitCommand(repoDir, commitCommand); + } + + private AbstractTreeIterator prepareTreeParser(Repository repository, String objectId) throws IOException { + // from the commit we can build the tree which allows us to construct the TreeParser + try (RevWalk walk = new RevWalk(repository)) { + RevCommit commit = walk.parseCommit(repository.resolve(objectId)); + RevTree tree = walk.parseTree(commit.getTree().getId()); + + CanonicalTreeParser oldTreeParser = new CanonicalTreeParser(); + try (ObjectReader oldReader = repository.newObjectReader()) { + oldTreeParser.reset(oldReader, tree.getId()); + } + + walk.dispose(); + + return oldTreeParser; + } + } + + public void commit(File repoDir, String message) { + if (!commitMessageIsValid(message)) { + System.err.println("Commit message isn't valid."); + return; + } + + File[] files = repoDir.listFiles(); + + Consumer commitCommand = git -> { + try { + addFiles(files, git); + commit(message, git); + System.out.printf("Committed file to repository at: %s%n", git.getRepository().getDirectory()); + } catch (GitAPIException e) { + e.printStackTrace(); + } + }; + + runGitCommand(repoDir, commitCommand); + } + + public void log(File repoDir) { + + Consumer logCommand = git -> { + try { + Iterable logs = git.log() + .call(); + + int count = 0; + for (RevCommit rev : logs) { + System.out.printf("Commit: %s, name: %s%n", rev, rev.getName()); + count++; + } + + System.out.printf("Had %d commits overall on current branch%n", count); + + } catch (NoHeadException e) { + System.err.printf("Repository %s is empty. Try to commit something.%n", git.getRepository().getDirectory()); + } catch (GitAPIException e) { + e.printStackTrace(); + } + }; + + runGitCommand(repoDir, logCommand); + } + + private boolean commitMessageIsValid(String message) { + return !message.trim().isEmpty(); + } + + private void addFiles(File[] files, Git git) throws GitAPIException { + for (File file : files) { + git.add() + .addFilepattern(file.getName()) + .call(); + } + } + + private void commit(String message, Git git) throws GitAPIException { + git.commit() + .setMessage(message) + .call(); + } + + private void runGitCommand(File repoDir, Consumer command) { + File gitDir = new File(repoDir, ".git"); + if (gitDir.exists()) { + repoDir = gitDir; + } + + FileRepositoryBuilder builder = new FileRepositoryBuilder(); + try (Repository repository = builder.setGitDir(repoDir) + .readEnvironment() // scan environment GIT_* variables + .findGitDir(repoDir) // scan up the file system tree + .setMustExist(true) + .build()) { + + try (Git git = new Git(repository)) { + command.accept(git); + } + + } catch (RepositoryNotFoundException e) { + System.err.printf("Repository not found: %s. Try create git repository.%n", repoDir); + } catch (IOException e) { + e.printStackTrace(); + } + + } + +} diff --git a/app/src/processing/app/git/GitOutputParser.java b/app/src/processing/app/git/GitOutputParser.java new file mode 100644 index 00000000000..afd999816e6 --- /dev/null +++ b/app/src/processing/app/git/GitOutputParser.java @@ -0,0 +1,93 @@ +package processing.app.git; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +public class GitOutputParser { + + /** + * It divides diff output in two parts. + * @param in stream with diff text + * @return result of parsing + */ + public ParserResult diffParser(InputStream in) { + DiffText newText = new DiffText(); + DiffText oldText = new DiffText(); + + StringBuilder newBuilder = new StringBuilder(); + StringBuilder oldBuilder = new StringBuilder(); + + Scanner scanner = new Scanner(in); + while (scanner.hasNext()) { + String string = scanner.nextLine(); + if (string.startsWith("@@")) { + break; + } + } + + while (scanner.hasNext()) { + String str = scanner.nextLine(); + + if (str.startsWith("+")) { + newText.getBeginIndexes().add(newBuilder.length()); + newBuilder.append(str.substring(1)).append("\n"); + newText.getEndIndexes().add(newBuilder.length()); + continue; + } + + if (str.startsWith("-")) { + oldText.getBeginIndexes().add(oldBuilder.length()); + oldBuilder.append(str.substring(1)).append("\n"); + oldText.getEndIndexes().add(oldBuilder.length()); + continue; + } + + newBuilder.append(str).append("\n"); + oldBuilder.append(str).append("\n"); + } + + newText.text = newBuilder.toString(); + oldText.text = oldBuilder.toString(); + + return new ParserResult(newText, oldText); + } + + public static class ParserResult { + private DiffText newText; + private DiffText oldText; + + public ParserResult(DiffText newText, DiffText oldText) { + this.newText = newText; + this.oldText = oldText; + } + + public DiffText getNewText() { + return newText; + } + + public DiffText getOldText() { + return oldText; + } + } + + public static class DiffText { + private String text; + private List beginIndexes = new ArrayList<>(); + private List endIndexes = new ArrayList<>(); + + public String getText() { + return text; + } + + public List getBeginIndexes() { + return beginIndexes; + } + + public List getEndIndexes() { + return endIndexes; + } + } + +}