GitPullActivity.java 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. package info.knacki.pass.ui;
  2. import android.os.Bundle;
  3. import android.support.v7.app.AppCompatActivity;
  4. import android.widget.ProgressBar;
  5. import android.widget.TextView;
  6. import java.io.File;
  7. import java.io.FileOutputStream;
  8. import java.io.IOException;
  9. import java.io.OutputStream;
  10. import java.util.HashMap;
  11. import java.util.HashSet;
  12. import java.util.Map;
  13. import java.util.Set;
  14. import java.util.Stack;
  15. import java.util.logging.Level;
  16. import java.util.logging.Logger;
  17. import info.knacki.pass.R;
  18. import info.knacki.pass.git.GitInterface;
  19. import info.knacki.pass.git.GitInterfaceFactory;
  20. import info.knacki.pass.git.GitLocal;
  21. import info.knacki.pass.git.GitSha1;
  22. import info.knacki.pass.git.entities.GitCommit;
  23. import info.knacki.pass.git.entities.GitObject;
  24. import info.knacki.pass.io.FileUtils;
  25. import info.knacki.pass.io.OnResponseListener;
  26. import info.knacki.pass.io.OnStreamResponseListener;
  27. import info.knacki.pass.io.PathUtils;
  28. import info.knacki.pass.settings.SettingsManager;
  29. import info.knacki.pass.ui.alertPrompt.AlertPrompt;
  30. import info.knacki.pass.ui.alertPrompt.AlertPromptGenerator;
  31. import info.knacki.pass.ui.alertPrompt.views.ConflictView;
  32. public class GitPullActivity extends AppCompatActivity {
  33. private final static Logger log = Logger.getLogger(GitPullActivity.class.getName());
  34. public final static String COMMIT_MSG = "Android pass sync";
  35. public static String LOCAL_GIT_HASH_VERSION_FILE;
  36. private GitInterface fGitInterface;
  37. private GitCommit fHeadCommit;
  38. private void OnMsg(final String msg) {
  39. log.info(msg);
  40. GitPullActivity.this.runOnUiThread(() -> {
  41. TextView logView = findViewById(R.id.logView);
  42. logView.append(msg +"\n");
  43. });
  44. }
  45. @Override
  46. protected void onCreate(Bundle savedInstanceState) {
  47. super.onCreate(savedInstanceState);
  48. final SettingsManager.VCS versioning = SettingsManager.GetVCS(this);
  49. setContentView(R.layout.activity_git_pull);
  50. setTitle(R.string.pref_vcs_git_pull);
  51. if (!(versioning instanceof SettingsManager.Git)) {
  52. finish();
  53. return;
  54. }
  55. LOCAL_GIT_HASH_VERSION_FILE = PathUtils.GetGitFile(this);
  56. try {
  57. fGitInterface = GitInterfaceFactory.factory((SettingsManager.Git) versioning);
  58. fGitInterface.FetchHead(new OnStreamResponseListener<GitCommit>() {
  59. @Override
  60. public void OnMsg(final String msg) {
  61. GitPullActivity.this.OnMsg(msg);
  62. }
  63. @Override
  64. public void OnResponse(GitCommit result) {
  65. fHeadCommit = result;
  66. GitPullActivity.this.runOnUiThread(GitPullActivity.this::OnTreeStructureFetched);
  67. }
  68. @Override
  69. public void OnError(final String msg, Throwable e) {
  70. GitPullActivity.this.runOnUiThread(() -> {
  71. OnMsg(msg);
  72. FinishLoading();
  73. });
  74. }
  75. });
  76. } catch (GitInterfaceFactory.GitInterfaceException e) {
  77. GitPullActivity.this.runOnUiThread(() -> {
  78. OnMsg(e.getMessage());
  79. FinishLoading();
  80. });
  81. }
  82. findViewById(R.id.close_bt).setOnClickListener(v -> GitPullActivity.this.finish());
  83. }
  84. protected void OnTreeStructureFetched() {
  85. final GitLocal localVersion = new GitLocal(new File(LOCAL_GIT_HASH_VERSION_FILE));
  86. HashMap<String, GitObject.GitBlob> filesToPull = new HashMap<>();
  87. Set<String> filesToPush = new HashSet<>();
  88. HashMap<String, GitObject.GitBlob> conflictingFiles = new HashMap<>();
  89. OnMsg("Done reading remote tree");
  90. OnMsg("Building change list");
  91. for (Map.Entry<String, GitObject.GitBlob>i: fHeadCommit.GetTree().FindAllBlobs().entrySet()) {
  92. final String remoteKnownHash = localVersion.GetHash(i.getKey(), "");
  93. final String remoteHash = GitSha1.BytesToString(i.getValue().GetHash());
  94. final String currentHash = GitSha1.BytesToString(GitSha1.getRawSha1OfFile(new File(PathUtils.GetPassDir(this) +i.getKey())));
  95. final boolean remoteChanged = !remoteKnownHash.equals(remoteHash);
  96. final boolean localChanged = !remoteKnownHash.equals(currentHash);
  97. if (remoteChanged && localChanged) {
  98. // Conflict (but file can still has the same content)
  99. if (currentHash.equals(remoteHash))
  100. filesToPull.put(i.getKey(), i.getValue());
  101. else
  102. conflictingFiles.put(i.getKey(), i.getValue());
  103. } else if (remoteChanged) {
  104. // remote changed
  105. filesToPull.put(i.getKey(), i.getValue());
  106. } else if (localChanged) {
  107. // local changed
  108. filesToPush.add(i.getKey());
  109. }
  110. }
  111. for (String i: localVersion.FileNames()) {
  112. if (fHeadCommit.GetTree().GetObjectFullPath(i) == null) {
  113. log.finer("removed from remote " +i);
  114. final String currentHash = GitSha1.BytesToString(GitSha1.getRawSha1OfFile(new File(PathUtils.GetPassDir(this) +i)));
  115. final boolean localChanged = !currentHash.equals(localVersion.GetHash(i, ""));
  116. if (!localChanged || "".equals(currentHash))
  117. filesToPull.put(i, null);
  118. else
  119. conflictingFiles.put(i, null);
  120. }
  121. }
  122. CheckNewFiles(localVersion, new File(PathUtils.GetPassDir(this)), "", filesToPull.keySet(), filesToPush, conflictingFiles.keySet());
  123. if (conflictingFiles.isEmpty())
  124. SyncFiles(localVersion, filesToPull, filesToPush);
  125. else
  126. AskForConflicts(localVersion, conflictingFiles, filesToPull, filesToPush);
  127. }
  128. int CheckNewFiles(GitLocal localVersion, File root, String rootPath, Set<String> filesToPull, Set<String> filesToPush, Set<String> conflicts) {
  129. int newFiles = 0;
  130. for (final File i: root.listFiles()) {
  131. if (!PathUtils.IsHidden(i.getAbsolutePath())) {
  132. if (i.isDirectory()) {
  133. newFiles += CheckNewFiles(localVersion, i, rootPath + "/" + i.getName(), filesToPull, filesToPush, conflicts);
  134. } else if (i.isFile()) {
  135. String path = rootPath + "/" + i.getName();
  136. if (!localVersion.HasHash(path) && !filesToPull.contains(path) && !conflicts.contains(path)) {
  137. // New file
  138. filesToPush.add(path);
  139. newFiles++;
  140. }
  141. }
  142. }
  143. }
  144. return newFiles;
  145. }
  146. void AskForConflicts(final GitLocal localVersion, final HashMap<String, GitObject.GitBlob> conflicts, final HashMap<String, GitObject.GitBlob> filesToPull, final Set<String> filesToPush) {
  147. runOnUiThread(() -> {
  148. AlertPrompt pt = AlertPromptGenerator.StaticMake(GitPullActivity.this)
  149. .setCancelable(true)
  150. .setNegativeButton(R.string.cancel, (dialogInterface, view) -> GitPullActivity.this.finish())
  151. .setPositiveButton(R.string.ok, (dialogInterface, v) -> {
  152. ConflictView.ConflictViewResult viewResult = ((ConflictView) v).GetResult();
  153. for (String s: viewResult.fUseTheir) {
  154. filesToPull.put(s, conflicts.get(s));
  155. }
  156. filesToPush.addAll(viewResult.fUseMine);
  157. SyncFiles(localVersion, filesToPull, filesToPush);
  158. })
  159. .setTitle(R.string.conflictingFiles);
  160. ConflictView view = new ConflictView(GitPullActivity.this, pt, conflicts.keySet());
  161. pt.setView(view).show();
  162. view.UpdateButtonState();
  163. });
  164. }
  165. void RmEmptyDirs(File dir, boolean isRoot) {
  166. File[] content = dir.listFiles();
  167. if (null == content || content.length == 0) {
  168. if (!isRoot)
  169. FileUtils.DeleteFile(dir);
  170. return;
  171. }
  172. for (File i: content)
  173. if (i.isDirectory())
  174. RmEmptyDirs(i, false);
  175. }
  176. void FinishLoading() {
  177. ProgressBar pg = findViewById(R.id.progressBar);
  178. pg.setIndeterminate(false);
  179. pg.setMax(1);
  180. pg.setProgress(1);
  181. RmEmptyDirs(new File(PathUtils.GetPassDir(GitPullActivity.this)), true);
  182. findViewById(R.id.close_bt).setEnabled(true);
  183. }
  184. void SyncFiles(final GitLocal localVersion, final HashMap<String, GitObject.GitBlob> filesToPull, final Set<String> filesToPush) {
  185. final OnStreamResponseListener<Void> allDone = new OnStreamResponseListener<Void>() {
  186. @Override
  187. public void OnResponse(Void result) {
  188. GitPullActivity.this.runOnUiThread(() -> {
  189. localVersion.Write(new File(LOCAL_GIT_HASH_VERSION_FILE));
  190. FinishLoading();
  191. });
  192. }
  193. @Override
  194. public void OnError(String msg, Throwable e) {
  195. GitPullActivity.this.runOnUiThread(() -> FinishLoading());
  196. }
  197. @Override
  198. public void OnMsg(String message) {
  199. GitPullActivity.this.OnMsg(message);
  200. }
  201. };
  202. final Runnable afterFetching = () -> {
  203. if (filesToPush.size() > 0) {
  204. GitPullActivity.this.OnMsg("Updating remote repository");
  205. PushBlobs(filesToPush, localVersion, allDone);
  206. } else {
  207. GitPullActivity.this.OnMsg("Nothing to push");
  208. allDone.OnResponse(null);
  209. }
  210. };
  211. if (filesToPull.isEmpty()) {
  212. OnMsg("Nothing to pull");
  213. afterFetching.run();
  214. } else {
  215. OnMsg("Updating local repository");
  216. DownloadBlobs(filesToPull, localVersion, new OnStreamResponseListener<Void>() {
  217. @Override
  218. public void OnResponse(Void result) {
  219. afterFetching.run();
  220. }
  221. @Override
  222. public void OnError(String msg, Throwable e) {
  223. allDone.OnError(msg, e);
  224. }
  225. @Override
  226. public void OnMsg(String message) {
  227. GitPullActivity.this.OnMsg(message);
  228. }
  229. });
  230. }
  231. }
  232. void DownloadBlobs(Map<String, GitObject.GitBlob> blobs, final GitLocal localVersion, final OnStreamResponseListener<Void> resp) {
  233. if (blobs.size() == 0) {
  234. resp.OnResponse(null);
  235. return;
  236. }
  237. final TextView logView = findViewById(R.id.logView);
  238. logView.append(blobs.size() +" files to update locally\n");
  239. for (String i: blobs.keySet())
  240. logView.append(" > " +i +"\n");
  241. final Stack<Map.Entry<String, GitObject.GitBlob>> files = new Stack<>();
  242. files.addAll(blobs.entrySet());
  243. final OnStreamResponseListener<byte[]> downloader = new OnStreamResponseListener<byte[]>() {
  244. @Override
  245. public void OnResponse(final byte[] result) {
  246. final String filename = files.peek().getKey();
  247. final GitObject.GitBlob blob = files.peek().getValue();
  248. final OnStreamResponseListener<byte[]> _this = this;
  249. GitPullActivity.this.runOnUiThread(() -> {
  250. WriteFile(filename, localVersion, blob, result);
  251. logView.append("Done fetching " +files.peek().getValue().GetFilename() +"\n");
  252. files.pop();
  253. DownloadNext(files, localVersion, _this, resp);
  254. });
  255. }
  256. @Override
  257. public void OnError(final String msg, Throwable e) {
  258. log.log(Level.SEVERE, msg, e);
  259. GitPullActivity.this.runOnUiThread(() -> logView.append("Error while fetching " +files.peek().getValue().GetFilename() +": " +msg));
  260. files.pop();
  261. if (!files.empty()) {
  262. fGitInterface.FetchBlob(files.peek().getValue(), this);
  263. } else {
  264. resp.OnResponse(null);
  265. }
  266. }
  267. @Override
  268. public void OnMsg(String message) {
  269. resp.OnMsg(message);
  270. }
  271. };
  272. DownloadNext(files, localVersion, downloader, resp);
  273. }
  274. void DownloadNext(Stack<Map.Entry<String, GitObject.GitBlob>> files, GitLocal localCache, OnStreamResponseListener<byte[]> downloader, OnResponseListener<Void> resp) {
  275. if (!files.empty()) {
  276. if (files.peek().getValue() == null) {
  277. // remove file
  278. String filename = files.pop().getKey();
  279. FileUtils.TrashFile(new File(PathUtils.GetPassDir(this) +filename));
  280. localCache.remove(filename);
  281. DownloadNext(files, localCache, downloader, resp);
  282. log.info("Removed file " +filename);
  283. } else {
  284. fGitInterface.FetchBlob(files.peek().getValue(), downloader);
  285. }
  286. } else {
  287. resp.OnResponse(null);
  288. }
  289. }
  290. void WriteFile(String filename, final GitLocal localVersion, GitObject.GitBlob blob, byte[] result) {
  291. File f = new File(PathUtils.GetPassDir(this) +filename);
  292. FileUtils.MkDir(f.getParentFile(), true);
  293. final int chunkSize = 1024;
  294. final int nbChunk = (int) Math.ceil((double) result.length / chunkSize);
  295. try {
  296. OutputStream writer = new FileOutputStream(f);
  297. for (int i = 0; i < nbChunk; ++i)
  298. writer.write(result, i * chunkSize, Math.min(chunkSize, result.length -Math.max(0, ((i -1) * chunkSize))));
  299. writer.close();
  300. }
  301. catch (IOException e) {
  302. log.log(Level.SEVERE, e.getMessage(), e);
  303. return;
  304. }
  305. localVersion.SetHash(filename, GitSha1.BytesToString(blob.GetHash()));
  306. }
  307. void PushBlobs(final Set<String> files, final GitLocal localVersion, final OnStreamResponseListener<Void> resp) {
  308. if (files.isEmpty()) {
  309. resp.OnMsg("Nothing to commit");
  310. resp.OnResponse(null);
  311. } else {
  312. SettingsManager.Git config = (SettingsManager.Git) SettingsManager.GetVCS(this);
  313. final GitCommit.Builder commit = new GitCommit.Builder(fHeadCommit, config.GetUsername(), config.GetUserEmail(), COMMIT_MSG);
  314. for (String i : files)
  315. commit.AddFile(i, new File(PathUtils.GetPassDir(this) + i));
  316. fGitInterface.PushCommitBuilder(commit, new OnStreamResponseListener<Void>() {
  317. @Override
  318. public void OnMsg(String message) {
  319. resp.OnMsg(message);
  320. }
  321. @Override
  322. public void OnResponse(Void result) {
  323. final GitObject.GitTree tree = commit.Build().GetTree();
  324. for (String i: files) {
  325. if (tree.GetObjectFullPath(i) == null)
  326. localVersion.remove(i);
  327. else
  328. localVersion.SetHash(i, GitSha1.BytesToString(tree.GetObjectFullPath(i).GetHash()));
  329. }
  330. OnMsg("Done");
  331. resp.OnResponse(result);
  332. }
  333. @Override
  334. public void OnError(String msg, Throwable e) {
  335. log.log(Level.SEVERE, msg, e);
  336. }
  337. });
  338. }
  339. }
  340. }