001package org.vishia.gitGui;
002
003import java.io.File;
004import java.io.FileNotFoundException;
005import java.io.IOException;
006import java.text.SimpleDateFormat;
007import java.util.Date;
008import java.util.LinkedList;
009import java.util.List;
010import java.util.Map;
011
012import org.vishia.cmd.CmdExecuter;
013import org.vishia.cmd.JZtxtcmdFilepath;
014import org.vishia.gral.base.GralButton;
015import org.vishia.gral.base.GralGraphicTimeOrder;
016import org.vishia.gral.base.GralTable;
017import org.vishia.gral.base.GralTextBox;
018import org.vishia.gral.base.GralTextField;
019import org.vishia.gral.base.GralWindow;
020import org.vishia.gral.ifc.GralTableLine_ifc;
021import org.vishia.gral.ifc.GralUserAction;
022import org.vishia.gral.ifc.GralWindow_ifc;
023import org.vishia.util.DataAccess;
024import org.vishia.util.Debugutil;
025import org.vishia.util.FileList;
026import org.vishia.util.FilePath;
027import org.vishia.util.FileSystem;
028import org.vishia.util.KeyCode;
029import org.vishia.util.StringPartAppend;
030
031
032/**A Graphical User Interface for basic working in git with remote repository and preserving time stamp of the files.
033 * <ul>
034 * <li>Show a working tree with remote repository (option -git-dir on any git command call).
035 * <li>Lists all changed files from working tree and between any revisions.
036 * <li>supports commit: via a text file for the commit text (edit with any standard editor) and commit button.
037 *   On any commit creates a new "_filelist.lst" via {@link FileList} which contains the time stamp of all files.
038 * <li>restore any file from any older version: select version, select file, right mouse "restore". (git checkout)
039 *   with restoring the original time stamp.
040 * </ul>
041 * @author Hartmut Schorrig
042 *
043 */
044public class GitGui
045{
046
047
048
049  /**Version, history and license
050   * <ul>
051   * <li>2018-10-28 Hartmut new textfield for cmd, shows the last automatically cmd, enables assembled cmd for git and common cmd. 
052   * <li>2018-10-27 Hartmut new {@link #openNewFileSelector(String, RevisionEntry)} not ready yet. 
053   * <li>2018-10-27 Hartmut chg uses <code>diff --name-status</diff> instead <code>diff --name-only</diff> 
054   *   to build the input for the #wdgTableFiles to show different files. The table has an additional left row for the kind of difference
055   *   adequate the output of <code>diff --name-status</diff>. 
056   * <li>2018-10-10 Hartmut new {@link #guiRepository(JZtxtcmdFilepath)} as start operation. It checks whether "name.gitRepository" is given
057   *  and copies "name.gitignore" to ".gitignore" and uses "name.filelist" as filelist. It supports more as one component on one working dir.
058   * <li>2018-10-10 Hartmut new {@link #getFilePathRepository(File)}. It is invoked in jzTc to get the opened repository by this GUI for add command.
059   * <li>2017-05-10 Hartmut bugfix {@link CmdExecuter#setCharsetForOutput(String)} to UTF-8 because git outputs cmd output in UTF-8
060   * <li>2016-12-02 Hartmut chg GitGui some improvements.
061   * <li>2016-09-23 Hartmut GitGui: ContextMenu in file table
062   * <li>2016-08-18 Hartmut this version is able to use to view the repository versions, the changed files per version, the changed file to the working tree.
063   *   It supports view diff with invocation of an external tool. It is the first productive version. But yet with some specific settings yet now.
064   *   TODO: read a config. Document it. Show the git command line for any action.
065   * <li>2016-08-24 Hartmut created
066   * </ul>
067   * 
068   * 
069   * <b>Copyright/Copyleft</b>:
070   * For this source the LGPL Lesser General Public License,
071   * published by the Free Software Foundation is valid.
072   * It means:
073   * <ol>
074   * <li> You can use this source without any restriction for any desired purpose.
075   * <li> You can redistribute copies of this source to everybody.
076   * <li> Every user of this source, also the user of redistribute copies
077   *    with or without payment, must accept this license for further using.
078   * <li> But the LPGL is not appropriate for a whole software product,
079   *    if this source is only a part of them. It means, the user
080   *    must publish this part of source,
081   *    but don't need to publish the whole source of the own product.
082   * <li> You can study and modify (improve) this source
083   *    for own using or for redistribution, but you have to license the
084   *    modified sources likewise under this LGPL Lesser General Public License.
085   *    You mustn't delete this Copyright/Copyleft inscription in this source file.
086   * </ol>
087   * If you are intent to use this sources without publishing its usage, you can get
088   * a second license subscribing a special contract with the author. 
089   * 
090   * @author Hartmut Schorrig = hartmut.schorrig@vishia.de
091   * 
092   * 
093   */
094  public final String sVersion = "2018-10-28";  
095
096
097  static class RevisionEntry
098  {
099    final String revisionHash;
100    String treeHash;
101    String parentHash;
102    String author;
103    Date dateAuthor;
104    String committer;
105    Date dateCommit;
106    String commitTitle;
107    StringBuilder commitText = new StringBuilder(200);
108    
109    RevisionEntry(String hash) { revisionHash = hash; }
110    
111  }
112
113
114
115  static class Settings
116  {
117    
118    File dirTemp1 = new File("t:/git_tmp1");
119    File dirTemp2 = new File("t:/git_tmp2");
120  }
121
122
123  /**Action for open the commit text. 
124   * 
125   */
126  GralUserAction actionOpenCommitText = new GralUserAction("actionTableOpenCommitText")
127  { @Override public boolean exec(int actionCode, org.vishia.gral.ifc.GralWidget_ifc widgd, Object... params) {
128      String[] args ={"cmd.exe", "/C", "edit", ".gitCommit"};
129      wdgCmd.setText("cmd.exe /C edit .gitCommit");
130      gitCmd.addCmd(args, null, listOut, null, workingDir, null);
131      wdgCommit.setText("do commit");
132      return true;
133  } };
134
135
136  /**This code snippet is executed after the 'git diff' command for 2 revisions are executed. 
137   * It is used as last argument of {@link CmdExecuter#execute(String[], boolean, String, List, List, org.vishia.cmd.CmdExecuter.ExecuteAfterFinish)}
138   * and prepares the {@link #wdgTableFiles}.
139   */
140  CmdExecuter.ExecuteAfterFinish exec_CommitDone = new CmdExecuter.ExecuteAfterFinish()
141  { @Override
142    public void exec(int errorcode, Appendable out, Appendable err)
143    { if(errorcode ==0) {
144        FileSystem.writeFile("\n", sWorkingDir + "/.gitcommit");
145        wdgCommit.setText("commit done");
146      } else {
147        wdgCommit.setText("commit error");
148      }
149    }
150  }; 
151
152
153
154  /**Action for do commi. 
155   * 
156   */
157  GralUserAction actionCommit = new GralUserAction("actionCommit")
158  { @Override public boolean exec(int actionCode, org.vishia.gral.ifc.GralWidget_ifc widgd, Object... params) {
159      File gitCommit = new File(sWorkingDir, ".gitCommit");
160      if(gitCommit.length() > 3) {
161        try{ FileList.list(sWorkingDir, "*", GitGui.this.sFileList);
162        } catch(Exception exc){
163          wdgInfo.setText("_filelist.lst problem: " + exc.getMessage());
164        }
165        String sGitCmd = "git";
166        if(! sGitDir.startsWith(sWorkingDir)) {
167          sGitCmd += " '--git-dir=" + sGitDir + "'";
168        }
169        sGitCmd += " commit -a -F .gitcommit";
170        wdgCmd.setText(sGitCmd);
171        String[] args ={exepath.gitsh_exe, "-x", "-c", sGitCmd};
172        gitCmd.addCmd(args, null, listOut, null, workingDir, exec_CommitDone);
173      } else {
174        wdgCommit.setText("do commit text?");
175      }
176      return true;
177  } };
178
179
180  /**Action for show the version table for the given file. 
181   * 
182   */
183  GralUserAction actionRefresh = new GralUserAction("actionRefresh")
184  { @Override public boolean exec(int actionCode, org.vishia.gral.ifc.GralWidget_ifc widgd, Object... params) {
185      if(KeyCode.isControlFunctionMouseUpOrMenu(actionCode)){ 
186        startLog(null); return true;
187      } //if;
188      return false;
189  } };
190
191
192
193
194  GralUserAction actionTableLineVersion = new GralUserAction("actionTablelineVersion")
195  { @Override public boolean exec(int actionCode, org.vishia.gral.ifc.GralWidget_ifc widgd, Object... params) {
196      if(actionCode == KeyCode.userSelect) {
197        @SuppressWarnings("unchecked")
198        GralTable<RevisionEntry>.TableLineData line = (GralTable<RevisionEntry>.TableLineData) params[0];
199        
200        showLog_startRevisionDiff4FileTable(line);
201      }  
202      return true;
203    }
204  };
205
206
207  /**This code snippet is executed after the 'git diff' command for 2 revisions are executed. 
208   * It is used as last argument of {@link CmdExecuter#execute(String[], boolean, String, List, List, org.vishia.cmd.CmdExecuter.ExecuteAfterFinish)}
209   * and prepares the {@link #wdgTableFiles}.
210   */
211  CmdExecuter.ExecuteAfterFinish exec_fillRevisionTable = new CmdExecuter.ExecuteAfterFinish()
212  { @Override
213    public void exec(int errorcode, Appendable out, Appendable err)
214    { fillRevisionTable();
215    }
216  };
217
218
219  /**This code snippet is executed after the 'git diff' command for 2 revisions are executed. 
220   * It is used as last argument of {@link CmdExecuter#execute(String[], boolean, String, List, List, org.vishia.cmd.CmdExecuter.ExecuteAfterFinish)}
221   * and prepares the {@link #wdgTableFiles}.
222   */
223  CmdExecuter.ExecuteAfterFinish exec_fillFileTable4Revision = new CmdExecuter.ExecuteAfterFinish()
224  { @Override
225    public void exec(int errorcode, Appendable out, Appendable err)
226    { fillFileTable4Revision();
227    }
228  };
229
230
231  /**This code snippet is executed after the 'git diff' command for 2 revisions are executed. 
232   * It is used as last argument of {@link CmdExecuter#execute(String[], boolean, String, List, List, org.vishia.cmd.CmdExecuter.ExecuteAfterFinish)}
233   * and prepares the {@link #wdgTableFiles}.
234   */
235  CmdExecuter.ExecuteAfterFinish exec_ShowStatus = new CmdExecuter.ExecuteAfterFinish()
236  { @Override
237    public void exec(int errorcode, Appendable out, Appendable err)
238    { wdgInfo.setText(gitOut);  //The status info
239      gitOut.buffer().setLength(0);  //prepare for the next command.
240      gitOut.assign(gitOut.buffer());   //to reset positions to the changed gitOut.buffer()
241    }
242  };
243
244
245  /**Action for mouse double to start view diff. 
246   * 
247   */
248  GralUserAction actionTableFile = new GralUserAction("actionTableFile")
249  { @Override public boolean exec(int actionCode, org.vishia.gral.ifc.GralWidget_ifc widgd, Object... params) {
250      @SuppressWarnings("unchecked")
251      GralTable<RevisionEntry>.TableLineData line = (GralTable<RevisionEntry>.TableLineData)params[0];  //it is the table line.
252       
253      String sFile = line.getKey(); //"org/vishia/inspcPC/InspCmd.java";
254      switch(actionCode) {
255      case (KeyCode.ctrl | KeyCode.enter):
256      case KeyCode.mouse1Double: startDiffView(sFile, GitGui.this.currentEntry, GitGui.this.cmpEntry); return true;
257      case (KeyCode.ctrl | 's'): startLog(sFile); return true;
258      default: return false;
259      } //switch;
260      
261  } };
262
263
264  /**Action for mouse double to start view diff. 
265   * 
266   */
267  GralUserAction actionRestore = new GralUserAction("actionRestore")
268  { @Override public boolean exec(int actionCode, org.vishia.gral.ifc.GralWidget_ifc widgd, Object... params) {
269      @SuppressWarnings("unchecked")
270      GralTable<RevisionEntry> table = (GralTable<RevisionEntry>)widgd;  //it is the table line.
271      if(KeyCode.isControlFunctionMouseUpOrMenu(actionCode)){ 
272        GralTable<RevisionEntry>.TableLineData line = table.getLineMousePressed(); //(GralTable<RevisionEntry>.TableLineData)params[0];  //it is the table line.
273        String sFile = line.getKey(); 
274        restoreFile(sFile); 
275        return true;
276      } //if;
277      return false;
278  } };
279
280
281  /**Action for mouse double to start view diff. 
282   * 
283   */
284  GralUserAction actionTableFileDiffView = new GralUserAction("actionTableFileDiffView")
285  { @Override public boolean exec(int actionCode, org.vishia.gral.ifc.GralWidget_ifc widgd, Object... params) {
286      @SuppressWarnings("unchecked")
287      GralTable<RevisionEntry> table = (GralTable<RevisionEntry>)widgd;  //it is the table line.
288      
289      GralTable<RevisionEntry>.TableLineData lineCurr = table.getCurrentLine(); //(GralTable<RevisionEntry>.TableLineData)params[0];  //it is the table line.
290      GralTable<RevisionEntry>.TableLineData line = table.getLineMousePressed(); //(GralTable<RevisionEntry>.TableLineData)params[0];  //it is the table line.
291      if(KeyCode.isControlFunctionMouseUpOrMenu(actionCode)){ 
292        String sFile = line.getKey(); 
293        startDiffView(sFile, GitGui.this.currentEntry, GitGui.this.cmpEntry); return true;
294      } //if;
295      return false;
296  } };
297
298
299  GralUserAction actionTableFileRenMove = new GralUserAction("actionTableFileRenMove")
300  { @Override public boolean exec(int actionCode, org.vishia.gral.ifc.GralWidget_ifc widgd, Object... params) {
301      @SuppressWarnings("unchecked")
302      GralTable<RevisionEntry> table = (GralTable<RevisionEntry>)widgd;  //it is the table line.
303      
304      GralTable<RevisionEntry>.TableLineData lineCurr = table.getCurrentLine(); //(GralTable<RevisionEntry>.TableLineData)params[0];  //it is the table line.
305      GralTable<RevisionEntry>.TableLineData line = table.getLineMousePressed(); //(GralTable<RevisionEntry>.TableLineData)params[0];  //it is the table line.
306      if(KeyCode.isControlFunctionMouseUpOrMenu(actionCode)){ 
307        String sFile = line.getKey(); 
308        openNewFileSelector(sFile, GitGui.this.currentEntry); return true;
309      } //if;
310      return false;
311  } };
312
313
314  /**Action for diff view of the current file to the workspace.*/
315  GralUserAction actionDiffCurrWork = new GralUserAction("actionCurrFileDiffView")
316  { @Override public boolean exec(int actionCode, org.vishia.gral.ifc.GralWidget_ifc widgd, Object... params) {
317      if(KeyCode.isControlFunctionMouseUpOrMenu(actionCode)){ 
318        GralTable<RevisionEntry>.TableLineData line = wdgTableVersion.getCurrentLine();
319        RevisionEntry revision = line.getData();
320        startDiffView(sLocalFile, null, revision); return true;
321      } //if;
322      return false;
323  } };
324
325
326  /**Action for diff view of the current file between revisions.*/
327  GralUserAction actionFileDiffRev = new GralUserAction("actionFileDiffRev")
328  { @Override public boolean exec(int actionCode, org.vishia.gral.ifc.GralWidget_ifc widgd, Object... params) {
329      if(KeyCode.isControlFunctionMouseUpOrMenu(actionCode)){ 
330        GralTable<RevisionEntry>.TableLineData line = wdgTableVersion.getCurrentLine();
331        RevisionEntry revision = line.getData();
332        startDiffView(sLocalFile, null, revision); return true;
333      } //if;
334      return false;
335  } };
336
337
338  /**Action for show the version table for the given file. 
339   * 
340   */
341  GralUserAction actionTableFileLog = new GralUserAction("actionTableFileLog")
342  { @Override public boolean exec(int actionCode, org.vishia.gral.ifc.GralWidget_ifc widgd, Object... params) {
343      @SuppressWarnings("unchecked")
344      GralTable<RevisionEntry> table = (GralTable<RevisionEntry>)widgd;  //it is the table line.
345      GralTable<RevisionEntry>.TableLineData lineCurr = table.getCurrentLine(); //(GralTable<RevisionEntry>.TableLineData)params[0];  //it is the table line.
346      GralTable<RevisionEntry>.TableLineData line = table.getLineMousePressed(); //(GralTable<RevisionEntry>.TableLineData)params[0];  //it is the table line.
347      if(KeyCode.isControlFunctionMouseUpOrMenu(actionCode)){ 
348        String sFile = line.getKey();   //working tree has key "*"
349        startLog(sFile); return true;
350      } //if;
351      return false;
352  } };
353
354
355  GralUserAction actionExecCmd = new GralUserAction("actionExecCmd")
356  { @Override public boolean exec(int actionCode, org.vishia.gral.ifc.GralWidget_ifc widgd, Object... params) {
357      if(KeyCode.isControlFunctionMouseUpOrMenu(actionCode)){ 
358        String cmd = wdgCmd.getText();
359        File cmdDir = workingDir;
360        int posDir = cmd.indexOf('>');
361        if(posDir >0) {
362          cmdDir = new File(cmd.substring(0, posDir));
363          cmd = cmd.substring(posDir+1);
364        }
365        String[] args = cmd.split(" ");
366        if(args[0].equals("git")) {
367          String[] args2 = args;
368          args = new String[4];
369          args[0] = exepath.gitsh_exe;
370          args[1] = "-x";
371          args[2] = "-c";
372          args[3] = cmd;
373        }
374        gitOut.clear();
375        gitCmd.addCmd(args, null, listOut, null, cmdDir, execShowListOut);
376
377      } //if;
378      return false;
379  } };
380
381  /**Action for show the version table for the given file. 
382   * 
383   */
384  GralUserAction actionAdd = new GralUserAction("actionAdd")
385  { @Override public boolean exec(int actionCode, org.vishia.gral.ifc.GralWidget_ifc widgd, Object... params) {
386      if(KeyCode.isControlFunctionMouseUpOrMenu(actionCode)){ 
387        addFileFromSelection(); return true;
388      } //if;
389      return false;
390  } };
391
392
393
394  /**Action for show the version table for the given file. 
395   * 
396   */
397  GralUserAction actionMove = new GralUserAction("actionMove")
398  { @Override public boolean exec(int actionCode, org.vishia.gral.ifc.GralWidget_ifc widgd, Object... params) {
399      if(KeyCode.isControlFunctionMouseUpOrMenu(actionCode)){ 
400        moveFileListToSelection(); return true;
401      } //if;
402      return false;
403  } };
404
405
406
407
408  CmdExecuter.ExecuteAfterFinish execShowListOut = new CmdExecuter.ExecuteAfterFinish()
409  { @Override
410    public void exec(int errorcode, Appendable out, Appendable err)
411    { //if(errorcode ==0) {
412      wdgInfo.setText("cmd ouptut:\n");
413      try { wdgInfo.append(gitOut);
414      } catch (IOException e) { }
415  } };
416
417
418  /**The paths to the executables. */
419  GitGuiPaths exepath;
420  
421  /**The only one opened git repository. null if nothing is open or more as one is open.
422   * 
423   */
424  static File filePathRepository;
425  
426  Settings settings = new Settings();
427
428  String sTypeOfImplementation = "SWT";  //default
429  
430
431  GralWindow window = new GralWindow("0+60, 0+90", "GitGui", "Git vishia", GralWindow_ifc.windResizeable | GralWindow_ifc.windRemoveOnClose);
432
433  GralTextField wdgCmd = new GralTextField("@2-2,0..-7=cmd", GralTextField.Type.editable);
434  
435  GralButton wdgBtnCmd = new GralButton("@0..2, -6..0 = cmdExec", "exec", this.actionExecCmd);
436
437  
438  GralTable<RevisionEntry> wdgTableVersion = new GralTable<>("@3..-30,0..-40=git-versions", new int[] {2, 10, 0, -10});
439  
440  GralTable<String> wdgTableFiles = new GralTable<>("@3..-30,-40..0=git-files", new int[] {3,20,0});
441  
442  GralTextBox wdgInfo = new GralTextBox("@-30..0, 0..-20=info");
443
444  GralButton wdgBtnDiffCurrWork = new GralButton("@-29..-27, -18..-2 = diffCurrWork", "diff current file to workspace", this.actionDiffCurrWork);
445
446  GralButton wdgBtnDiffCurrFile = new GralButton("@-26..-24, -18..-2 = diffCurrFile", "diff current file", this.actionFileDiffRev);
447
448  GralButton wdgBtnAdd = new GralButton("@-18+2, -18..-8 = add", "add", this.actionAdd);
449
450  GralButton wdgBtnMove = new GralButton("@-15+2, -18..-8 = move", "mv", this.actionMove);
451
452  GralButton wdgRefresh = new GralButton("@-12+2, -18..-8 = refresh", "refresh", this.actionRefresh);
453
454  GralButton wdgCommitText = new GralButton("@-9+2, -18..-2 = commitText", "commit-text", this.actionOpenCommitText);
455
456  GralButton wdgCommit = new GralButton("@-6+2, -18..-2 = commit", "do commit", this.actionCommit);
457
458  /**If set to true, the {@link #cmdThread} should be aborted.
459   * 
460   */
461  boolean bCmdThreadClose;
462
463  CmdExecuter gitCmd = new CmdExecuter();
464
465
466  /**Destination for output of all command line invocations.
467   * This buffer will be cleared and filled with the git command, and then parsed to present the result. 
468   */
469  StringPartAppend gitOut = new StringPartAppend();
470  
471  /**The {@link CmdExecuter#execute(String[], boolean, String, List, List, org.vishia.cmd.CmdExecuter.ExecuteAfterFinish)}
472   * needs a list of appendable, that is it.*/
473  List<Appendable> listOut = new LinkedList<Appendable>();
474  { listOut.add(gitOut); }
475  
476  /**Stored arguments from {@link #startLog(String, String, String)}. */
477  String sGitDir, sWorkingDir; //, sLocalFile;
478
479  File workingDir;
480  
481  String sFileList = "_filelist.lst";
482
483  /**If given the file which's log and diff should be shown.
484   * Elsewhere null. set in {@link #startLog(String)}
485   */
486  String sLocalFile;
487
488
489  /**The presentation of the time stamps. */
490  SimpleDateFormat dateFormat = new SimpleDateFormat("yy-MM-dd HH:mm");
491
492
493
494  /**The current line and the line before, to the earlier commit. The Predecessor is not the parent in any case, not on branch and merge points. */
495  //GralTable<RevisionEntry>.TableLineData currentLine, cmpLine;
496  GralTableLine_ifc<RevisionEntry> currentLine, cmpLine;
497  
498  /**The current entry and the entry before, to the earlier commit. The Predecessor is not the parent in any case, not on branch and merge points. */
499  RevisionEntry currentEntry, cmpEntry;
500  
501
502  public static void main(String[] args){
503    GitGui main = new GitGui(args);
504    //only test
505    main.startLog("D:/GitArchive/D/vishia/srcJava_vishiaBase/.git", "D:/GitArchive/D/vishia/srcJava_vishiaBase", "org/vishia/util/CalculatorExpr.java");
506    main.doSomethinginMainthreadTillCloseWindow();
507  }
508  
509
510  public GitGui(String[] args) {
511    if(args !=null && args.length >=1){
512      sTypeOfImplementation = args[0];
513    }
514    initializeCmd();
515    if(!settings.dirTemp1.exists()) { settings.dirTemp1.mkdirs(); }
516    if(!settings.dirTemp2.exists()) { settings.dirTemp2.mkdirs(); }
517    wdgTableVersion.specifyActionOnLineSelected(actionTableLineVersion);
518    wdgTableFiles.specifyActionChange("actionTableFile", actionTableFile, null);
519    window.specifyActionOnCloseWindow(actionOnCloseWindow);
520    window.create(sTypeOfImplementation, 'B', null, initGraphic);
521    cmdThread.start();
522  }
523
524
525  GralGraphicTimeOrder initGraphic = new GralGraphicTimeOrder("init")
526  { @Override protected void executeOrder() {
527      wdgTableFiles.addContextMenuEntryGthread(1, "diffView", "Diff view [Mouse double], [ctrl-Enter]", actionTableFileDiffView);
528      wdgTableFiles.addContextMenuEntryGthread(1, "rename/move", "rename/move", actionTableFileRenMove);
529      wdgTableFiles.addContextMenuEntryGthread(1, "restore", "Restore this file", actionRestore);
530      wdgTableFiles.addContextMenuEntryGthread(1, "show Log for File", "Show log for this file [ctrl-s]", actionTableFileLog);
531    }
532  };
533
534
535
536
537  GralUserAction actionOnCloseWindow = new GralUserAction("")
538  { @Override public boolean exec(int actionCode, org.vishia.gral.ifc.GralWidget_ifc widgd, Object... params) {
539      GitGui.this.gitCmd.close();
540      bCmdThreadClose = true;
541      return true;
542    }
543  };
544
545
546  @Override public void finalize()
547  {
548  }
549
550  void initializeCmd() {
551    gitCmd.setCharsetForOutput("UTF-8");  //git outputs in UTF-8
552    Map<String, String> env = gitCmd.environment();
553    env.put("HOMEPATH", "\\vishia\\HOME");
554    env.put("HOMEDRIVE", "D:");
555    String sPath = env.get("PATH");
556    if(sPath ==null) { sPath = env.get("Path"); }
557    if(sPath ==null) { sPath = env.get("path"); }
558    sPath = "D:\\Programs\\Gitcmd\\bin;" + sPath;
559    env.put("PATH", sPath);
560  }
561
562
563  public void doSomethinginMainthreadTillCloseWindow()
564  { int ix = 0;
565    while(! window.isGraphicDisposed()){
566      try{ Thread.sleep(1000);} 
567      catch (InterruptedException e) { }
568    }
569    
570  }
571
572
573
574
575
576  /**Search the base dir and the repository for a given path/to/file.
577   * @param startFile Any file path. Especially a ".git" directory or ".gitRepository*" file. In that  case the local file path will be set to null,
578   *   because the whole repository is given.
579   *   A ".gitRepository*" file contains the path to the extern .git directory.
580   * @param dst maybe null, elsewhere new String[2]. 
581   *   dst[0] will be filled with the absolute path of the .git directory. 
582   *   dst[1] will be filled with the absolute path to the basic directory for the working tree where .git or .gitRepository were found.
583   *   dst[2] will be filled with the local file path or null if the .git or .gitRepositiory is the startFile.
584   * @param dstMap maybe null, if not null it is a container maybe from JZcmd variables or an empty Map.
585   * @param nameRepository if dstMap !=null, the name of the key (variable) for the repository path.
586   * @param nameBasedir if dstMap !=null, the name of the key (variable) for the base directory.
587   * @param nameLocalFile  if dstMap !=null, the name of the key (variable) for the local file.
588   *   That variables will be set. nameLocalFile will be set to null if .git or .gitRepository is the startFile.
589   * @return null on success or an error message 
590   */
591  public static String searchRepository(File startFile, String[] dst, Map<String, DataAccess.Variable<Object>> dstMap
592  , String nameRepository, String nameBasedir, String nameLocalFile) 
593  { File fRepo = null;
594    File currDir;
595    String ret = null;
596    if(startFile.isDirectory()) {
597      currDir = startFile;
598      fRepo = new File(startFile, ".git"); //only to check whether it exists
599      if(!fRepo.exists()){
600        fRepo = new File(startFile, ".gitRepository");
601        if(!fRepo.exists()) {  //if startFile as currdir contains .git or .gitRepository, it's okay
602          currDir = null;  //not okay, startFile is any directory maybe inside a working tree
603          fRepo = null;
604        }
605      }
606    } else { //startFile is a file.
607      String fName = startFile.getName();
608      if(fName.equals(".git") || fName.endsWith(".gitRepository")) {
609        fRepo = startFile;
610        currDir = startFile.getParentFile();
611      } else {
612        currDir = null; //startFile is not a directory, search in parents.
613      }
614    }
615    if(currDir == null) { //startFile is not a directory, or it is a directory which does not contain .git or .gitRepository 
616      currDir = startFile.getParentFile();
617      //search whether a .git or .gitRepository exists and change to parent dir till it is found.
618      do{
619        fRepo = new File(currDir, ".git"); //only to check whether it exists
620        if(!fRepo.exists()){
621          fRepo = new File(currDir, ".gitRepository");
622          if(!fRepo.exists()){
623            fRepo = null;
624            try{
625              currDir = FileSystem.getDirectory(currDir);  //NOTE: currDir.getParent() is not successfully on relative dir "."
626            } catch(FileNotFoundException exc){ currDir = null;}
627          }
628        }
629      } while(fRepo ==null && currDir !=null);
630    }
631    //
632    //currdir and fRepo should be set, or not:
633    if(fRepo == null){
634      ret = "searchRepository - .git or .gitRepository not found ;" + startFile.getAbsolutePath();
635    } else {
636      assert(currDir !=null); //set together with fRepo
637      String sBaseDir = FileSystem.normalizePath(currDir).toString();
638      //dst.put(bzrdir, sBzrDir);
639      String sRepository;
640      if(fRepo.getName().endsWith(".gitRepository")){   //File with link to repository
641        filePathRepository = fRepo;
642        sRepository = FileSystem.readFile(fRepo).trim();
643      } else {
644        sRepository = sBaseDir;  
645      }
646      CharSequence sFilePath = FileSystem.normalizePath(startFile);
647      String sLocalFilePath;
648      if(currDir == startFile) {
649        sLocalFilePath = null;
650      } else {
651        sLocalFilePath = sFilePath.subSequence(sBaseDir.length()+1, sFilePath.length()).toString();
652        if(sLocalFilePath.length() == 0 || sLocalFilePath.equals(".git") || sLocalFilePath.endsWith(".gitRepository")){
653          sLocalFilePath = null;  //no local file.
654        }
655      }
656      if(dst !=null) { 
657        dst[0] = sRepository; 
658        dst[1] = sBaseDir; 
659        dst[2] = sLocalFilePath; 
660      }
661      if(dstMap !=null) {
662        try {
663        DataAccess.createOrReplaceVariable(dstMap, nameBasedir, 'S', sBaseDir, true);
664        DataAccess.createOrReplaceVariable(dstMap, nameRepository, 'S', sRepository, true);
665        DataAccess.createOrReplaceVariable(dstMap, nameLocalFile, 'S', sLocalFilePath, true);
666        } catch(Exception exc) {
667          ret = "searchRepository - repository found, but write problems for dst Map,";
668        }
669      }
670    }
671    return ret;
672  }
673
674
675
676  /**Returns the opened repository or repoository linking file or searches the next .git or .gitRepository file
677   * in the parent dir. It is to return the correct repository linking file for the opened GUI
678   * to add something with Fcmd and jzTc
679   * @param dir The dir where a file should be handled.
680   * @return The opened filePathRepository only if the dir is in the same file tree.
681   *   Elsewhere it searches .git or .gitRepository in this dir and parents.
682   */
683  public static File getFilePathRepository(File startFileSearchRepository) {
684    if(filePathRepository !=null) {
685      String sFilePathRepository = FileSystem.getCanonicalPath(filePathRepository.getParentFile());
686      String sstartFileSearchRepository = FileSystem.getCanonicalPath(startFileSearchRepository);
687      if(sstartFileSearchRepository.startsWith(sFilePathRepository)) {
688        return filePathRepository;  //same or sub dir as filePathRepository: Proper.
689      }
690    }
691    //else:
692    return FileSystem.searchInParent(startFileSearchRepository, ".gitRepository", ".git");  //search either .gitRepository or .git
693  }
694  
695  
696
697
698  /**Searches the git repository and the root of the working tree and opens the window for the git gui. 
699   * This routine can be invoked from any Java program without additional conditions. 
700   * @param srcFile Any file to start searching the root of the working tree and the git repository inside this tree.
701   *   If it is a directory and that directory contains either ".git" or ".gitRepository", it is the root of the working tree.
702   *   Elsewhere the root will be searched backward to the root of the file system. 
703   *   It means you can start this routine with any srcFile inside the working tree.
704   *   This file will be used as additional argument for example to show the history of that file. 
705   */
706  public static void guiRepository(GitGuiPaths exepath, JZtxtcmdFilepath repoFile)
707  {
708    File srcFile = null;
709    try {
710      //copy to the working version of .gitignore   
711      File fIgnore = new File(repoFile.absname() + ".gitignore");
712      if(fIgnore.exists()) { 
713        File fIgnore2 = new File(repoFile.absdir() + "/.gitignore");
714        FileSystem.copyFile(fIgnore, fIgnore2);
715      }
716      srcFile = new File(repoFile.absfile().toString());
717    } catch (NoSuchFieldException | IOException e) { System.err.println(e.getMessage());}
718    String[] paths = new String[3];
719    String error = searchRepository(srcFile, paths, null, null, null, null);
720    if(error !=null) { System.err.println(error); }
721    else {
722      GitGui main = new GitGui(null);
723      main.exepath = exepath;
724      main.sFileList = repoFile.name() + ".filelist";
725      main.startLog(paths[0], paths[1], paths[2]);
726    }
727  }
728
729
730  /**Searches the git repository and the root of the working tree and opens the window for the git gui. 
731   * This routine can be invoked from any Java program without additional conditions. 
732   * @param srcFile Any file to start searching the root of the working tree and the git repository inside this tree.
733   *   If it is a directory and that directory contains either ".git" or ".gitRepository", it is the root of the working tree.
734   *   Elsewhere the root will be searched backward to the root of the file system. 
735   *   It means you can start this routine with any srcFile inside the working tree.
736   *   This file will be used as additional argument for example to show the history of that file. 
737   */
738  public static void showLog(File srcFile)
739  {
740    String[] paths = new String[3];
741    String error = searchRepository(srcFile, paths, null, null, null, null);
742    if(error !=null) { System.err.println(error); }
743    else {
744      GitGui main = new GitGui(null);
745      main.startLog(paths[0], paths[1], paths[2]);
746    }
747  }
748
749
750
751
752  public void startLog(String sGitDir, String sWorkingDir, String sLocalFile) {
753    this.sGitDir = sGitDir; 
754    this.sWorkingDir = sWorkingDir;
755    this.workingDir = new File(sWorkingDir);
756    startLog(sLocalFile);
757  }
758  
759  /**
760   * @param sLocalFile "*" for all files, else "path/in/loacal/tree/file.ext"
761   */
762  void startLog(String sLocalFile) {
763    //this.sLocalFile = sLocalFile;
764    String sPathShow;
765    this.sLocalFile = sLocalFile;
766    if(sLocalFile !=null) {
767      sPathShow = sGitDir + " : " + sLocalFile;
768      wdgBtnDiffCurrFile.setVisible(true);
769      wdgBtnDiffCurrWork.setVisible(true);
770    } else {
771      sPathShow = sGitDir;
772      wdgBtnDiffCurrFile.setVisible(false);
773      wdgBtnDiffCurrWork.setVisible(false);
774    }
775    window.setTitle("Git " + sPathShow);
776    
777    wdgTableVersion.clearTable();
778    wdgTableVersion.addLine("*", new String[] {"", "", "wait for prepairing log", ""}, null);
779    gitOut.buffer().setLength(0);
780    gitOut.assign(gitOut.buffer());   //to reset positions to the changed gitOut.buffer()
781    String sGitCmd = "git";
782    if(! sGitDir.startsWith(sWorkingDir)) {
783      sGitCmd += " '--git-dir=" + sGitDir + "'";
784    }
785    sGitCmd += " log --date=iso '--pretty=raw'";
786    if(sLocalFile !=null && sLocalFile.length() >0) {
787      sGitCmd +=  " -- '" + sLocalFile + "'";
788    }
789    wdgCmd.setText(sGitCmd);
790    String[] args ={exepath.gitsh_exe, "-x", "-c", sGitCmd};
791    gitCmd.clearCmdQueue();
792    gitCmd.abortCmd();
793    gitOut.buffer().setLength(0);
794    gitOut.assign(gitOut.buffer());   //to reset positions to the changed gitOut.buffer()
795    gitCmd.addCmd(args, null, listOut, null, workingDir, exec_fillRevisionTable);
796    synchronized(cmdThread) { cmdThread.notify(); }
797    //int error = cmd.execute(args, null,  gitOut, null);
798    //fillRevisionTable();
799  }
800
801
802
803
804  /**Fills the {@link #wdgTableVersion} with the gotten output after git log command. This routine is invoked as {@link #execAfterCmdLog}.
805   * 
806   */
807  void fillRevisionTable() {
808    gitOut.firstlineMaxpart();
809    String lineTexts[] = new String[4];
810    boolean contCommits = true;
811    wdgTableVersion.clearTable();
812    lineTexts[0] = "";
813    lineTexts[1] = "*";
814    lineTexts[2] = "--working area --";
815    lineTexts[3] = "";
816    wdgTableVersion.addLine("*", lineTexts, null);  
817    RevisionEntry entryLast = null;
818    do {
819      if(gitOut.scanStart().scan("commit ").scanOk()) {
820        contCommits = false;  //set to true on the next "commit " line.
821        String hash = gitOut.getCurrentPart().toString();
822        RevisionEntry entry = new RevisionEntry(hash);
823        try {
824          boolean cont = true;
825          while (cont && gitOut.nextlineMaxpart().found()) {
826            if(gitOut.scanStart().scan("tree ").scanOk()) {
827              entry.treeHash = gitOut.getCurrentPart().toString();
828            } 
829            else if(gitOut.scanStart().scan("parent ").scanOk()) {
830              entry.parentHash = gitOut.getCurrentPart().toString();
831            }
832            else if(gitOut.scanStart().scan("author ").scanOk()) {
833              gitOut.lento('<');
834              entry.author = gitOut.getCurrentPart().toString();
835              gitOut.fromEnd().seekPosBack(16).lento(' ');
836              if(gitOut.scan().scanInteger().scanOk()) { 
837                entry.dateAuthor = new Date(1000*gitOut.scan().getLastScannedIntegerNumber());
838              }
839            } else if(gitOut.scanStart().scan("committer ").scanOk()) {
840              gitOut.lento('<');
841              entry.committer = gitOut.getCurrentPart().toString();
842              gitOut.fromEnd().seekPosBack(16).lento(' ');
843              if(gitOut.scan().scanInteger().scanOk()) { 
844                entry.dateCommit = new Date(1000*gitOut.scan().getLastScannedIntegerNumber());
845              }
846            } else {
847              do {
848                if(gitOut.scanStart().scan("commit ").scanOk()) {
849                  gitOut.seekBegin();
850                  cont = false;
851                  contCommits = true;
852                  break;
853                } else {
854                  CharSequence commitTextline = gitOut.getCurrentPart();
855                  if(entry.commitTitle == null && commitTextline.length() >6){
856                    entry.commitTitle = commitTextline.toString();  //first line with at least 6 character: Use as title.
857                  }
858                  entry.commitText.append(commitTextline).append('\n');
859                }
860              } while(cont && (gitOut.nextlineMaxpart().found()));
861            }
862          }//while lines of one commit 
863        }catch(Exception exc) {
864          Debugutil.stop();
865        }
866        if(entry.dateAuthor !=null) {
867          String cL = "A";
868          if(entryLast !=null && entryLast.parentHash !=null && entryLast.parentHash.equals(entry.revisionHash)){ cL = "B"; }
869          lineTexts[0] = cL;
870          lineTexts[1] = dateFormat.format(entry.dateAuthor);
871          lineTexts[2] = entry.commitTitle;
872          lineTexts[3] = entry.author;
873          GralTableLine_ifc<RevisionEntry> line = wdgTableVersion.addLine(hash, lineTexts, entry);  
874          line.repaint();
875        }
876        entryLast = entry;
877      } //
878      else {
879        contCommits = gitOut.nextlineMaxpart().found();
880      }
881    } while(contCommits);
882    //wdgTableVersion.
883    wdgTableVersion.repaint();
884  }
885
886
887  /**Starts the command 'git diff' to fill the {@link #wdgTableFiles} for the selected revision.
888   * 
889   * @param line
890   */
891  void showLog_startRevisionDiff4FileTable(GralTable<RevisionEntry>.TableLineData line) {
892    if(line == null){
893       return;
894    }
895    //
896    //abort the current cmd for diff view (or any other)
897    gitCmd.clearCmdQueue();
898    gitCmd.abortCmd();
899    gitOut.buffer().setLength(0);
900    gitOut.assign(gitOut.buffer());   //to reset positions to the changed gitOut.buffer()
901    //
902    //
903    GitGui.this.currentLine = line;
904    GralTable<RevisionEntry> table = line.getTable();
905    List<GralTableLine_ifc<RevisionEntry>> markedLines = table.getMarkedLines(1);
906    GitGui.this.cmpLine = line.nextSibling();
907    if(markedLines !=null && markedLines.size() >0) {
908      if(markedLines.size() ==1) {
909        cmpLine = markedLines.get(0);
910      }
911      else {
912        System.err.println("more as one line marked");
913      }
914    }
915    GitGui.this.currentEntry = line.data;
916    GitGui.this.cmpEntry = cmpLine == null ? null : cmpLine.getUserData();
917    String sGitCmd = "git";
918    if(! sGitDir.startsWith(sWorkingDir)) {
919      sGitCmd += " '--git-dir=" + sGitDir + "'";
920    }
921    if(currentEntry ==null) {
922      String[] args ={exepath.gitsh_exe, "-x", "-c", sGitCmd + " status"};
923      gitCmd.addCmd(args, null, listOut, null, workingDir, exec_ShowStatus);
924      wdgInfo.setText("(working area)");
925      try{
926        wdgInfo.append("\n");
927      } catch(IOException exc){}
928      sGitCmd += " diff --name-status HEAD";
929    } else {
930      wdgInfo.setText(currentEntry.revisionHash);
931      try{
932        wdgInfo.append("=>").append(currentEntry.parentHash).append(" @").append(currentEntry.treeHash).append("\n");
933        if(cmpEntry !=null) {
934          wdgInfo.append(cmpEntry.revisionHash).append("=>").append(cmpEntry.parentHash).append(" @").append(cmpEntry.treeHash).append("\n");
935        }
936        wdgInfo.append(currentEntry.commitText);
937      } catch(IOException exc){}
938      sGitCmd += " diff --name-status " + currentEntry.parentHash + ".." + currentEntry.revisionHash;
939    }
940    wdgCmd.setText(sGitCmd);
941    String[] args ={exepath.gitsh_exe, "-x", "-c", sGitCmd};
942    //
943    gitCmd.addCmd(args, null, listOut, null, workingDir, exec_fillFileTable4Revision);
944    synchronized(cmdThread) { cmdThread.notify(); }
945  }
946  
947  
948
949  void restoreFile(String sFile) {
950    String sGitCmd2 = "git '--git-dir=" + sGitDir + "' checkout " + cmpEntry.revisionHash + " -- " + this.sFileList; ///
951    wdgCmd.setText(sGitCmd2);
952    String[] args2 ={exepath.gitsh_exe, "-x", "-c", sGitCmd2};
953    gitCmd.addCmd(args2, null, listOut, null, workingDir, null);
954
955    String sGitCmd = "git '--git-dir=" + sGitDir + "' checkout " + cmpEntry.revisionHash + " -- " + sFile;
956    String[] args ={exepath.gitsh_exe, "-x", "-c", sGitCmd};
957    gitCmd.addCmd(args, null, listOut, null, workingDir, 
958      new CmdExecuter.ExecuteAfterFinish()
959        { @Override
960          public void exec(int errorcode, Appendable out, Appendable err)
961          { try {FileList.touch(sWorkingDir, GitGui.this.sFileList, sFile, null);
962            } catch (IOException exc) {
963              System.err.println("IOexception for File.touch()");
964            }
965          }
966        }
967     );   
968
969
970
971    //
972    
973    //
974  }
975
976
977  
978  void addFileFromSelection() {
979    int pos = wdgInfo.getCursorPos();
980    String text = wdgInfo.getText();
981  
982    String sGitCmd = "git";
983    if(! sGitDir.startsWith(sWorkingDir)) {
984      sGitCmd += " '--git-dir=" + sGitDir + "'";
985    }
986    sGitCmd += " add ";
987    wdgCmd.setText(sGitCmd);
988  
989  }
990
991
992  void moveFileListToSelection() {
993    int pos = wdgInfo.getCursorPos();
994    String text = wdgInfo.getText();
995    
996    
997    
998    
999    GralTable<String>.TableLineData line = wdgTableFiles.getCurrentLine();
1000    int posSep = sGitDir.lastIndexOf('/');
1001    assert(sGitDir.substring(posSep).equals("/.git"));
1002    String sGitCmd = sGitDir.substring(0, posSep) + ">git mv ";
1003    sGitCmd += line.getCellText(2) + "/" + line.getCellText(1) + " ";
1004    
1005    wdgCmd.setText(sGitCmd);
1006
1007
1008  
1009  
1010  }
1011
1012
1013  /**Gets the file from repository and writes to tmp directory, starts diff tool
1014   * @param sFile The local path of the file
1015   * @param currRev maybe null, then compare with working tree, elsewhere a selected revision
1016   * @param cmpRev the selected revision to compare.
1017   */
1018  void startDiffView(String sFile,  RevisionEntry currRev, RevisionEntry cmpRev) {
1019    String sFile1 = settings.dirTemp1.getAbsolutePath() + "/" + sFile;
1020    String sFile2 = settings.dirTemp2.getAbsolutePath() + "/" + sFile;
1021    if(currRev == null) {
1022      sFile1 = sWorkingDir + "/" + sFile;
1023    } else  { 
1024      String sGitCmd = "git '--git-dir=" + sGitDir + "' checkout " + currRev.revisionHash + " -- " + sFile;
1025      String[] args ={exepath.gitsh_exe, "-x", "-c", sGitCmd};
1026      gitCmd.addCmd(args, null, listOut, null, settings.dirTemp1, null);
1027    }
1028    if(cmpRev == null) {
1029      sFile2 = sFile1;
1030    }
1031    else { 
1032      String sGitCmd = "git '--git-dir=" + sGitDir + "' checkout " + cmpRev.revisionHash + " -- " + sFile;
1033      wdgCmd.setText(settings.dirTemp2 + ">" + sGitCmd);
1034      String[] args ={exepath.gitsh_exe, "-x", "-c", sGitCmd};
1035      gitCmd.addCmd(args, null, listOut, null, settings.dirTemp2, null);
1036    }
1037    sFile1 = sFile1.replace('/', '\\');
1038    sFile2 = sFile2.replace('/', '\\');
1039    String sCmdDiff = "cmd.exe /C start " + exepath.diff_exe + " " + sFile1 + " " + sFile2;
1040    String[] cmdDiffView = CmdExecuter.splitArgs(sCmdDiff); 
1041    gitCmd.addCmd(cmdDiffView, null, listOut, null, null, null);
1042    synchronized(cmdThread) { cmdThread.notify(); }
1043  }
1044
1045
1046
1047  
1048  void openNewFileSelector(String sFile,  RevisionEntry currRev) {
1049    //wdgViewFiles.openDialog(FileRemote.fromFile(workingDir), "files", false, null);
1050  }
1051  
1052  
1053  
1054  
1055  /**Fills the file table with the gotten output after git diff name-status command. This routine is invoked as {@link #exec_fillFileTable4Revision}.
1056   * 
1057   */
1058  void fillFileTable4Revision() {
1059    wdgTableFiles.clearTable();
1060    GralTableLine_ifc<String> line = wdgTableFiles.addLine("*", new String[] {"!","(all files)",""}, "*");  
1061    wdgTableFiles.repaint();
1062    gitOut.firstlineMaxpart();
1063    do {
1064      String sLine = gitOut.getCurrentPart().toString();
1065      //the line starts with the status character, then '\t', then the file path.
1066      if(!sLine.startsWith("+ git") && sLine.length() >2) {
1067        String[] col = new String[3];
1068        int posSlash = sLine.lastIndexOf('/');
1069        col[0] = sLine.substring(0,1);
1070        if(posSlash >0) {
1071          col[1] = sLine.substring(posSlash+1);
1072          col[2] = sLine.substring(2, posSlash);
1073        } else {
1074          col[1] = sLine.substring(2);
1075          col[2] = "";
1076        }
1077        String key = sLine.substring(2); //without "M\t" (sign of change from <code>diff --name-status</code> output )
1078        line = wdgTableFiles.addLine(key, col, sLine);  
1079        line.repaint();
1080      }
1081    } while(gitOut.nextlineMaxpart().found());
1082  }
1083
1084
1085
1086  Thread cmdThread = new Thread("gitGui-Cmd") {
1087    @Override public void run() {
1088      do {
1089        gitCmd.executeCmdQueue(true);
1090        try {
1091          synchronized(this){ wait(1000); }
1092        } catch (InterruptedException e) { }
1093      } while (!bCmdThreadClose);
1094    }
1095  };
1096
1097
1098
1099
1100}