isundil 7 jaren geleden
bovenliggende
commit
c469d1ed52
34 gewijzigde bestanden met toevoegingen van 2778 en 0 verwijderingen
  1. 2 0
      .gitignore
  2. 38 0
      build.gradle
  3. 134 0
      gitdroid.iml
  4. 21 0
      proguard-rules.pro
  5. 26 0
      src/androidTest/java/info/knacki/gitdroid/ExampleInstrumentedTest.java
  6. 2 0
      src/main/AndroidManifest.xml
  7. 109 0
      src/main/java/info/knacki/gitdroid/BaseGitProtocol.java
  8. 14 0
      src/main/java/info/knacki/gitdroid/GitConfig.java
  9. 15 0
      src/main/java/info/knacki/gitdroid/GitInterface.java
  10. 34 0
      src/main/java/info/knacki/gitdroid/GitInterfaceFactory.java
  11. 78 0
      src/main/java/info/knacki/gitdroid/GitLocal.java
  12. 112 0
      src/main/java/info/knacki/gitdroid/GitSha1.java
  13. 238 0
      src/main/java/info/knacki/gitdroid/HttpGitProtocol.java
  14. 112 0
      src/main/java/info/knacki/gitdroid/Pacman.java
  15. 156 0
      src/main/java/info/knacki/gitdroid/PacmanBuilder.java
  16. 339 0
      src/main/java/info/knacki/gitdroid/SSHGitProtocol.java
  17. 5 0
      src/main/java/info/knacki/gitdroid/callback/OnErrorListener.java
  18. 5 0
      src/main/java/info/knacki/gitdroid/callback/OnResponseListener.java
  19. 5 0
      src/main/java/info/knacki/gitdroid/callback/OnStreamResponseListener.java
  20. 182 0
      src/main/java/info/knacki/gitdroid/entities/GitCommit.java
  21. 402 0
      src/main/java/info/knacki/gitdroid/entities/GitObject.java
  22. 29 0
      src/main/java/info/knacki/gitdroid/entities/GitPackable.java
  23. 28 0
      src/main/java/info/knacki/gitdroid/entities/GitPackableFactory.java
  24. 112 0
      src/main/java/info/knacki/gitdroid/entities/GitPackableUtil.java
  25. 38 0
      src/main/java/info/knacki/gitdroid/entities/GitRef.java
  26. 10 0
      src/main/java/info/knacki/gitdroid/entities/Util.java
  27. 64 0
      src/main/java/info/knacki/gitdroid/io/AppendableInputStream.java
  28. 242 0
      src/main/java/info/knacki/gitdroid/io/NetworkUtils.java
  29. 41 0
      src/main/java/info/knacki/gitdroid/io/OutputStreamWithCheckSum.java
  30. 135 0
      src/main/java/info/knacki/gitdroid/io/ssh/JSchWrapper.java
  31. 23 0
      src/main/java/info/knacki/gitdroid/io/ssh/SSHConnection.java
  32. 7 0
      src/main/java/info/knacki/gitdroid/io/ssh/SSHFactory.java
  33. 3 0
      src/main/res/values/strings.xml
  34. 17 0
      src/test/java/info/knacki/gitdroid/ExampleUnitTest.java

+ 2 - 0
.gitignore

@@ -32,3 +32,5 @@ proguard/
 # Android Studio captures folder
 captures/
 
+/build
+

+ 38 - 0
build.gradle

@@ -0,0 +1,38 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 28
+
+    defaultConfig {
+        minSdkVersion 15
+        targetSdkVersion 28
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+}
+
+dependencies {
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+    implementation 'com.android.support:appcompat-v7:28.0.0'
+    testImplementation 'junit:junit:4.12'
+    implementation 'org.bouncycastle:bcpg-jdk15on:1.60'
+    implementation 'com.jcraft:jsch:0.1.55'
+    implementation 'commons-http:commons-http:1.1'
+    androidTestImplementation 'com.android.support.test:runner:1.0.2'
+    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
+}

+ 134 - 0
gitdroid.iml

@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module external.linked.project.id=":gitdroid" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" type="JAVA_MODULE" version="4">
+  <component name="FacetManager">
+    <facet type="android-gradle" name="Android-Gradle">
+      <configuration>
+        <option name="GRADLE_PROJECT_PATH" value=":gitdroid" />
+      </configuration>
+    </facet>
+    <facet type="android" name="Android">
+      <configuration>
+        <option name="SELECTED_BUILD_VARIANT" value="release" />
+        <option name="ASSEMBLE_TASK_NAME" value="assembleRelease" />
+        <option name="COMPILE_JAVA_TASK_NAME" value="compileReleaseSources" />
+        <afterSyncTasks>
+          <task>generateReleaseSources</task>
+        </afterSyncTasks>
+        <option name="ALLOW_USER_CONFIGURATION" value="false" />
+        <option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
+        <option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
+        <option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
+        <option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
+        <option name="PROJECT_TYPE" value="1" />
+      </configuration>
+    </facet>
+  </component>
+  <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
+    <output url="file://$MODULE_DIR$/build/intermediates/javac/release/compileReleaseJavaWithJavac/classes" />
+    <output-test url="file://$MODULE_DIR$/build/intermediates/javac/releaseUnitTest/compileReleaseUnitTestJavaWithJavac/classes" />
+    <exclude-output />
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/source/apt/release" isTestSource="false" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/not_namespaced_r_class_sources/release/generateReleaseRFile/out" isTestSource="false" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/release" isTestSource="false" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/release" isTestSource="false" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/release" isTestSource="false" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/release" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/release" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/source/apt/test/release" isTestSource="true" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/release/res" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/release/resources" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/release/assets" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/release/aidl" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/release/java" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/release/rs" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/release/shaders" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/testRelease/res" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/testRelease/resources" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/testRelease/assets" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/testRelease/aidl" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/testRelease/java" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/testRelease/rs" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/testRelease/shaders" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/shaders" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/shaders" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/res" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/assets" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/aidl" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/rs" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/shaders" isTestSource="true" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/aapt_friendly_merged_manifests" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/annotation_processor_list" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/check-manifest" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/intermediate-jars" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/javac" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/jniLibs" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/library_assets" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint_jar" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/merged_manifests" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/packaged_res" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/public_res" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/shader_assets" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/shaders" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/transforms" />
+      <excludeFolder url="file://$MODULE_DIR$/build/outputs" />
+      <excludeFolder url="file://$MODULE_DIR$/build/tmp" />
+    </content>
+    <orderEntry type="jdk" jdkName="Android API 28 Platform" jdkType="Android SDK" />
+    <orderEntry type="sourceFolder" forTests="false" />
+    <orderEntry type="library" name="Gradle: com.android.support:customview-28.0.0" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:localbroadcastmanager-28.0.0" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:support-vector-drawable-28.0.0" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:interpolator-28.0.0" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:support-core-utils-28.0.0" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:support-core-ui-28.0.0" level="project" />
+    <orderEntry type="library" name="Gradle: com.jcraft:jsch:0.1.55@jar" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:slidingpanelayout-28.0.0" level="project" />
+    <orderEntry type="library" name="Gradle: android.arch.lifecycle:viewmodel-1.1.1" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:drawerlayout-28.0.0" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:coordinatorlayout-28.0.0" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:collections:28.0.0@jar" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:documentfile-28.0.0" level="project" />
+    <orderEntry type="library" name="Gradle: org.bouncycastle:bcpg-jdk15on:1.60@jar" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:swiperefreshlayout-28.0.0" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:cursoradapter-28.0.0" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:asynclayoutinflater-28.0.0" level="project" />
+    <orderEntry type="library" name="Gradle: android.arch.lifecycle:livedata-1.1.1" level="project" />
+    <orderEntry type="library" name="Gradle: android.arch.core:common:1.1.1@jar" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:versionedparcelable-28.0.0" level="project" />
+    <orderEntry type="library" name="Gradle: org.bouncycastle:bcprov-jdk15on:1.60@jar" level="project" />
+    <orderEntry type="library" scope="TEST" name="Gradle: junit:junit:4.12@jar" level="project" />
+    <orderEntry type="library" scope="TEST" name="Gradle: org.hamcrest:hamcrest-core:1.3@jar" level="project" />
+    <orderEntry type="library" name="Gradle: android.arch.core:runtime-1.1.1" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:print-28.0.0" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:loader-28.0.0" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:viewpager-28.0.0" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:support-fragment-28.0.0" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:support-annotations:28.0.0@jar" level="project" />
+    <orderEntry type="library" name="Gradle: android.arch.lifecycle:common:1.1.1@jar" level="project" />
+    <orderEntry type="library" name="Gradle: android.arch.lifecycle:livedata-core-1.1.1" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:support-compat-28.0.0" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:animated-vector-drawable-28.0.0" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:appcompat-v7-28.0.0" level="project" />
+    <orderEntry type="library" name="Gradle: android.arch.lifecycle:runtime-1.1.1" level="project" />
+    <orderEntry type="library" name="Gradle: commons-http:commons-http:1.1@jar" level="project" />
+  </component>
+</module>

+ 21 - 0
proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 26 - 0
src/androidTest/java/info/knacki/gitdroid/ExampleInstrumentedTest.java

@@ -0,0 +1,26 @@
+package info.knacki.gitdroid;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getTargetContext();
+
+        assertEquals("info.knacki.gitdroid.test", appContext.getPackageName());
+    }
+}

+ 2 - 0
src/main/AndroidManifest.xml

@@ -0,0 +1,2 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="info.knacki.gitdroid" />

+ 109 - 0
src/main/java/info/knacki/gitdroid/BaseGitProtocol.java

@@ -0,0 +1,109 @@
+package info.knacki.gitdroid;
+
+import java.nio.charset.Charset;
+
+import info.knacki.gitdroid.callback.OnResponseListener;
+import info.knacki.gitdroid.callback.OnStreamResponseListener;
+import info.knacki.gitdroid.entities.GitCommit;
+import info.knacki.gitdroid.entities.GitRef;
+
+public abstract class BaseGitProtocol implements GitInterface {
+    protected final GitConfig fConfig;
+
+    BaseGitProtocol(GitConfig config) {
+        fConfig = config;
+    }
+
+    abstract protected void PushPack(GitCommit commit, Pacman pack, String branch, OnStreamResponseListener<Void> resp);
+    abstract protected void FetchCommit(GitRef ref, OnStreamResponseListener<GitCommit> response);
+
+    void GetHeadCommitFromRef(GitRef myRef, final OnStreamResponseListener<GitCommit> response) {
+        if (myRef != null) {
+            response.OnMsg("Checking out branch " +myRef.GetBranchName() +" revision " +myRef.GetHash());
+            FetchCommit(myRef, new OnStreamResponseListener<GitCommit>() {
+                @Override
+                public void OnResponse(final GitCommit result) {
+                    response.OnResponse(result);
+                }
+
+                @Override
+                public void OnMsg(String message) {
+                    response.OnMsg(message);
+                }
+
+                @Override
+                public void OnError(String msg, Throwable e) {
+                    response.OnError(msg, e);
+                }
+            });
+        } else {
+            response.OnError("Branch " +fConfig.GetBranch() + " not found on remote", null);
+        }
+    }
+
+    protected GitRef GetRefFromBranchName(GitRef[] refs, String branchName) {
+        if (refs != null)
+            for (GitRef ref: refs)
+                if (ref.GetBranch().equals(branchName))
+                    return ref;
+        return null;
+    }
+
+    GitRef GetHeadRef(GitRef[] refs) {
+        return GetRefFromBranchName(refs, fConfig.GetBranch());
+    }
+
+    public void FetchHead(final OnStreamResponseListener<GitCommit> response) {
+        GetRefs(new OnResponseListener<GitRef[]>() {
+            @Override
+            public void OnResponse(final GitRef[] result) {
+                GitRef myRef = GetHeadRef(result);
+                GetHeadCommitFromRef(myRef, response);
+            }
+
+            @Override
+            public void OnError(final String msg, final Throwable e) {
+                response.OnError(msg, e);
+            }
+        });
+    }
+
+    byte[] GitLine(String line) {
+        String lineResult;
+        if (line == null) {
+            lineResult = "0000";
+        } else {
+            lineResult = String.format("%04x%s\n", line.length() +5, line);
+        }
+        return lineResult.getBytes(Charset.defaultCharset());
+    }
+
+    @Override
+    public void PushCommitBuilder(GitCommit.Builder commit, OnStreamResponseListener<Void> resp) {
+        PushPack(commit.Build(), new Pacman(commit.PreparePack()), fConfig.GetBranch(), resp);
+    }
+
+    protected abstract void OnBranchCreated();
+
+    @Override
+    public void CreateBranch(String branchName, OnStreamResponseListener<GitRef[]> callback) {
+        final GitCommit.Builder commit = new GitCommit.Builder(fConfig.GetUsername(), fConfig.GetUserEmail(), "init");
+        PushPack(commit.Build(), new Pacman(commit.PreparePack()), "refs/heads/" +branchName, new OnStreamResponseListener<Void>() {
+            @Override
+            public void OnMsg(String message) {
+                callback.OnMsg(message);
+            }
+
+            @Override
+            public void OnResponse(Void result) {
+                OnBranchCreated();
+                GetRefs(callback);
+            }
+
+            @Override
+            public void OnError(String msg, Throwable e) {
+                callback.OnError(msg, e);
+            }
+        });
+    }
+}

+ 14 - 0
src/main/java/info/knacki/gitdroid/GitConfig.java

@@ -0,0 +1,14 @@
+package info.knacki.gitdroid;
+
+import info.knacki.gitdroid.io.NetworkUtils;
+
+public interface GitConfig extends NetworkUtils.AuthConfig {
+    String GetBranch();
+    String GetUsername();
+    String GetUserEmail();
+    boolean HasAuthentication();
+    String GetUser();
+    String GetPassword();
+    String GetPrivateKey();
+    String GetUrl();
+}

+ 15 - 0
src/main/java/info/knacki/gitdroid/GitInterface.java

@@ -0,0 +1,15 @@
+package info.knacki.gitdroid;
+
+import info.knacki.gitdroid.callback.OnResponseListener;
+import info.knacki.gitdroid.callback.OnStreamResponseListener;
+import info.knacki.gitdroid.entities.GitCommit;
+import info.knacki.gitdroid.entities.GitObject;
+import info.knacki.gitdroid.entities.GitRef;
+
+public interface GitInterface {
+    void GetRefs(OnResponseListener<GitRef[]> callback);
+    void FetchHead(final OnStreamResponseListener<GitCommit> response);
+    void FetchBlob(GitObject.GitBlob blob, OnStreamResponseListener<byte[]> response);
+    void PushCommitBuilder(GitCommit.Builder commit, OnStreamResponseListener<Void> resp);
+    void CreateBranch(String branchName, OnStreamResponseListener<GitRef[]> callback);
+}

+ 34 - 0
src/main/java/info/knacki/gitdroid/GitInterfaceFactory.java

@@ -0,0 +1,34 @@
+package info.knacki.gitdroid;
+
+import android.support.annotation.NonNull;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class GitInterfaceFactory {
+    private final static Logger log = Logger.getLogger(GitInterfaceFactory.class.getName());
+
+    public static class GitInterfaceException extends Throwable {
+        GitInterfaceException(String msg) {
+            super(msg);
+        }
+
+        GitInterfaceException(Throwable e) {
+            super(e.getMessage(), e);
+        }
+    }
+
+    public static @NonNull  GitInterface factory(GitConfig config) throws GitInterfaceException {
+        if (config == null)
+            throw new GitInterfaceException("No configuration found");
+        try {
+            if (config.GetUrl().startsWith("http://") || config.GetUrl().startsWith("https://"))
+                return new HttpGitProtocol(config);
+            return new SSHGitProtocol(config);
+        }
+        catch (Throwable e) {
+            log.log(Level.SEVERE, "Cannot create git interface: " +e.getMessage(), e);
+            throw new GitInterfaceException(e);
+        }
+    }
+}

+ 78 - 0
src/main/java/info/knacki/gitdroid/GitLocal.java

@@ -0,0 +1,78 @@
+package info.knacki.gitdroid;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class GitLocal {
+    final private static Logger log = Logger.getLogger(GitLocal.class.getName());
+    final private Map<String, String> cache;
+
+    public GitLocal(File file) {
+        cache = new HashMap<>();
+        BufferedReader in;
+
+        try {
+            in = new BufferedReader(new FileReader(file));
+        }
+        catch (FileNotFoundException e) {
+            return;
+        }
+        String line;
+        try {
+            while ((line = in.readLine()) != null) {
+                int lineSep = line.indexOf(' ');
+                if (lineSep > -1) {
+                    cache.put(line.substring(lineSep +1), line.substring(0, lineSep));
+                }
+            }
+            in.close();
+        }
+        catch (IOException e) {
+            log.log(Level.WARNING, e.getMessage(), e);
+        }
+    }
+
+    public String GetHash(String key, String defaultValue) {
+        String res = cache.get(key);
+        return res == null ? defaultValue : res;
+    }
+
+    public void SetHash(String filename, String hash) {
+        cache.put(filename, hash);
+    }
+
+    public Set<String> FileNames() {
+        return cache.keySet();
+    }
+
+    public GitLocal remove(String filename) {
+        cache.remove(filename);
+        return this;
+    }
+
+    public void Write(File f) {
+        try {
+            f.createNewFile();
+            FileWriter writer = new FileWriter(f);
+            for (HashMap.Entry<String, String> i: cache.entrySet())
+                writer.write(i.getValue() +" " +i.getKey() +"\n");
+            writer.close();
+        }
+        catch (IOException e) {
+            log.log(Level.SEVERE, "Write local git index file error: " +e.getMessage(), e);
+        }
+    }
+
+    public boolean HasHash(String path) {
+        return cache.containsKey(path);
+    }
+}

+ 112 - 0
src/main/java/info/knacki/gitdroid/GitSha1.java

@@ -0,0 +1,112 @@
+package info.knacki.gitdroid;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import info.knacki.gitdroid.entities.GitObject;
+import info.knacki.gitdroid.entities.GitPackable;
+import info.knacki.gitdroid.entities.GitPackableUtil;
+
+public class GitSha1 {
+    private final static Logger log = Logger.getLogger(GitSha1.class.getName());
+
+    public static byte[] StringToBytes(String hash) {
+        byte[] result = new byte[20];
+        int j =0;
+
+        for (int i =0; i +1 < hash.length(); i += 2)
+            result[j++] = (byte) Integer.parseInt(hash.substring(i, i+2), 16);
+        return result;
+    }
+
+    public static String BytesToString(byte[] sha1Bytes) {
+        return BytesToString(sha1Bytes, 0);
+    }
+
+    public static String BytesToString(byte[] sha1Bytes, int offset) {
+        StringBuilder sb = new StringBuilder();
+        for (int i =offset; i < Math.min(sha1Bytes.length -offset, offset +20); ++i)
+            sb.append(Integer.toString((sha1Bytes[i] & 0xff) + 0x100, 16).substring(1));
+        return sb.toString();
+    }
+
+    public static byte[] getRawSha1OfFile(final File f)
+    {
+        return getRawSha1OfPackable(new GitPackable() {
+            @Override
+            public eType GetPackableType() {
+                return eType.eType_Blob;
+            }
+
+            @Override
+            public byte[] GetPack() {
+                try {
+                    return GitObject.GitBlob.GetFilePack(f);
+                }
+                catch (FileNotFoundException e) {
+                    return null;
+                }
+                catch (IOException e) {
+                    log.log(Level.SEVERE, "Cannot compute sha1 sum for file " +f.getAbsolutePath() +": " +e.getMessage(), e);
+                    return null;
+                }
+            }
+        });
+    }
+
+    public static byte[] getRawSha1OfPackable(GitPackable obj) {
+        try {
+            MessageDigest sha1Builder = MessageDigest.getInstance("SHA1");
+            byte[] packContent = obj.GetPack();
+            if (packContent == null) {
+                return new byte[]{};
+            }
+            sha1Builder.update((GitPackableUtil.GetObjTypeString(obj.GetPackableType()) +" " +packContent.length).getBytes());
+            sha1Builder.update(new byte[] { 0 });
+            sha1Builder.update(packContent);
+            return sha1Builder.digest();
+        }
+        catch (NoSuchAlgorithmException e) {
+            return new byte[]{};
+        }
+    }
+
+    public static String getSha1OfPackable(GitPackable obj) {
+        byte[] raw = getRawSha1OfPackable(obj);
+        return raw.length == 0 ? "" : BytesToString(raw);
+    }
+
+    public static byte[] GetRawSha1OfData(GitPackable.eType type, byte[] data) {
+        try {
+            MessageDigest sha1Builder = MessageDigest.getInstance("SHA1");
+            sha1Builder.update((GitPackableUtil.GetObjTypeString(type) +" " +data.length).getBytes());
+            sha1Builder.update(new byte[] { 0 });
+            sha1Builder.update(data);
+            return sha1Builder.digest();
+        }
+        catch (NoSuchAlgorithmException e) {
+            return new byte[]{};
+        }
+    }
+
+    public static String GetSha1OfData(GitPackable.eType type, byte[] data) {
+        byte[] raw = GetRawSha1OfData(type, data);
+        return raw.length == 0 ? "" : BytesToString(raw);
+    }
+
+    public static boolean CompareChecksum(byte[] a, byte[] b) {
+        return CompareChecksum(a, b, 0);
+    }
+
+    public static boolean CompareChecksum(byte[] checksum, byte[] data, int offset) {
+        for (int i=0; i < checksum.length; ++i)
+            if (data[offset +i] != checksum[i])
+                return false;
+        return true;
+    }
+}

+ 238 - 0
src/main/java/info/knacki/gitdroid/HttpGitProtocol.java

@@ -0,0 +1,238 @@
+package info.knacki.gitdroid;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import info.knacki.gitdroid.callback.OnResponseListener;
+import info.knacki.gitdroid.callback.OnStreamResponseListener;
+import info.knacki.gitdroid.entities.GitCommit;
+import info.knacki.gitdroid.entities.GitObject;
+import info.knacki.gitdroid.entities.GitRef;
+import info.knacki.gitdroid.io.NetworkUtils;
+
+class HttpGitProtocol extends BaseGitProtocol {
+    private GitRef[] fRefsCache = null;
+    private static final Logger log = Logger.getLogger(HttpGitProtocol.class.getName());
+
+    HttpGitProtocol(GitConfig config) {
+        super(config);
+    }
+
+    public void GetRefs(final OnResponseListener<GitRef[]> callback) {
+        if (fRefsCache != null) {
+            callback.OnResponse(fRefsCache);
+            return;
+        }
+        try {
+            URL url = new URL(fConfig.GetUrl() + "/info/REFS");
+            NetworkUtils.protoGet(url, fConfig, new OnResponseListener<String>() {
+                @Override
+                public void OnResponse(String result) {
+                    if (result.length() == 0) {
+                        fRefsCache = new GitRef[0];
+                    } else {
+                        String[] refStrings = result.split("\n");
+                        fRefsCache = new GitRef[refStrings.length];
+                        for (int i = 0; i < refStrings.length; ++i) {
+                            String refLine = refStrings[i];
+                            for (int j = 0; j < refLine.length(); ++j) {
+                                if (refLine.charAt(j) == '\t') {
+                                    fRefsCache[i] = new GitRef(refLine.substring(0, j).trim(), refLine.substring(j + 1).trim());
+                                    break;
+                                }
+                            }
+                            if (fRefsCache[i] == null) {
+                                fRefsCache = null;
+                                callback.OnError("Cannot read references from repository", null);
+                                return;
+                            }
+                        }
+                    }
+                    callback.OnResponse(fRefsCache);
+                }
+
+                @Override
+                public void OnError(String msg, Throwable e) {
+                    callback.OnError(msg, e);
+                }
+            });
+        } catch (MalformedURLException e) {
+            callback.OnError(e.getMessage(), e);
+        }
+    }
+
+    public void FetchTree(final GitCommit ci, final OnStreamResponseListener<GitObject.GitTree> response) {
+        class RecursiveFetchTreeWalker implements OnStreamResponseListener<byte[]> {
+            private final GitObject.GitTree fRoot;
+            private GitObject.GitTree fCurrentTree;
+            private final OnStreamResponseListener<GitObject.GitTree> fResponseListener;
+
+            private RecursiveFetchTreeWalker(GitCommit ci, final OnStreamResponseListener<GitObject.GitTree> responseListener) {
+                fRoot = fCurrentTree = ci.InitializeTree().Initialize();
+                fResponseListener = responseListener;
+            }
+
+            public void run() {
+                PullHash(ci.GetTreeHash());
+            }
+
+            @Override
+            public void OnResponse(byte[] result) {
+                fCurrentTree.Fill(result);
+                fCurrentTree = fRoot.FindNextNotInitializedTree();
+                if (fCurrentTree != null)
+                    PullHash(fCurrentTree.Initialize().GetHash());
+                else
+                    fResponseListener.OnResponse(fRoot);
+            }
+
+            @Override
+            public void OnError(String msg, Throwable e) {
+                fResponseListener.OnError(msg, e);
+            }
+
+            @Override
+            public void OnMsg(String message) {
+                fResponseListener.OnMsg(message);
+            }
+
+            private void PullHash(String hash) {
+                fResponseListener.OnMsg("Reading tree " +hash);
+                HttpGitProtocol.this.PullHash(hash, this);
+            }
+
+            private void PullHash(byte[] hash) {
+                PullHash(GitSha1.BytesToString(hash));
+            }
+        }
+
+        new RecursiveFetchTreeWalker(ci, response).run();
+    }
+
+    public void FetchBlob(final GitObject.GitBlob blob, final OnStreamResponseListener<byte[]> response) {
+        PullHash(GitSha1.BytesToString(blob.GetHash()), response);
+    }
+
+    protected void PullHash(String hash, final OnStreamResponseListener<byte[]> response) {
+        URL url;
+        try {
+            url = new URL(fConfig.GetUrl() + "/objects/" + hash.substring(0, 2) + "/" + hash.substring(2));
+        }
+        catch (MalformedURLException e) {
+            response.OnError(e.getClass().getName() +": " +e.getMessage(), e);
+            return;
+        }
+        NetworkUtils.protoInflateGet(url, fConfig, response);
+    }
+
+    public void FetchCommit(final GitRef ref, final OnStreamResponseListener<GitCommit> response) {
+        PullHash(ref.GetHash(), new OnStreamResponseListener<byte[]>() {
+            @Override
+            public void OnResponse(byte[] result) {
+                GitCommit ci = new GitCommit(ref.GetHash(), new String(result, Charset.defaultCharset()));
+                response.OnMsg("Finished read commit");
+                response.OnMsg(ci.GetMessage());
+                response.OnMsg("Reading tree #" +GitSha1.BytesToString(ci.GetTreeHash()));
+                FetchTree(ci, new OnStreamResponseListener<GitObject.GitTree>() {
+                    @Override
+                    public void OnMsg(String message) {
+                        response.OnMsg(message);
+                    }
+
+                    @Override
+                    public void OnResponse(GitObject.GitTree tree) {
+                        response.OnMsg("Finished reading tree");
+                        response.OnResponse(ci);
+                    }
+
+                    @Override
+                    public void OnError(String msg, Throwable e) {
+                        response.OnError(msg, e);
+                    }
+                });
+            }
+
+            @Override
+            public void OnError(String msg, Throwable e) {
+                response.OnError(msg, e);
+            }
+
+            @Override
+            public void OnMsg(String message) {
+                response.OnMsg(message);
+            }
+        });
+    }
+
+    @Override
+    public void PushPack(final GitCommit commit, Pacman pack, String branch, final OnStreamResponseListener<Void> response) {
+        GetRefs(new OnResponseListener<GitRef[]>() {
+            @Override
+            public void OnResponse(final GitRef[] result) {
+                final GitRef myRef = GetRefFromBranchName(result, branch);
+                if (myRef == null)
+                    response.OnMsg("Creating branch " +branch);
+                else
+                    response.OnMsg("Pushing over " +myRef.GetBranch() +" revision " +myRef.GetHash());
+                try {
+                    HashMap<String, String> headers = new HashMap<>();
+                    headers.put("Content-Type", "application/x-git-receive-pack-request");
+                    NetworkUtils.ProtoPost(new URL(fConfig.GetUrl() +"/git-receive-pack"), headers, fConfig, new OnResponseListener<byte[]>() {
+                        @Override
+                        public void OnResponse(byte[] result) {
+                            response.OnResponse(null);
+                        }
+
+                        @Override
+                        public void OnError(String msg, Throwable e) {
+                            log.log(Level.SEVERE, "git-receive-pack error", e);
+                            response.OnError(msg, e);
+                        }
+                    }, new OnResponseListener<OutputStream>() {
+                        @Override
+                        public void OnResponse(OutputStream result) {
+                            try {
+                                String prev = myRef == null ? "0000000000000000000000000000000000000000 " : (myRef.GetHash() +" ");
+                                result.write(GitLine(prev +GitSha1.BytesToString(GitSha1.getRawSha1OfPackable(commit)) +" " +branch));
+                                result.write(GitLine(null));
+                                if (!pack.Write(result)) {
+                                    OnError("Pack error", null);
+                                    return;
+                                }
+                                result.close();
+                            }
+                            catch (IOException e) {
+                                log.log(Level.SEVERE, "Cannot git-upload-pack: " +e.getMessage(), e);
+                                OnError(e.getMessage(), e);
+                            }
+                        }
+
+                        @Override
+                        public void OnError(String msg, Throwable e) {
+                            response.OnError(msg, e);
+                        }
+                    });
+                }
+                catch (IOException e) {
+                    response.OnError(e.getMessage(), e);
+                }
+            }
+
+            @Override
+            public void OnError(final String msg, final Throwable e) {
+                response.OnError(msg, e);
+            }
+        });
+    }
+
+    @Override
+    protected void OnBranchCreated() {
+        fRefsCache = null;
+    }
+}

+ 112 - 0
src/main/java/info/knacki/gitdroid/Pacman.java

@@ -0,0 +1,112 @@
+package info.knacki.gitdroid;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayDeque;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import info.knacki.gitdroid.entities.GitCommit;
+import info.knacki.gitdroid.entities.GitObject;
+import info.knacki.gitdroid.entities.GitPackable;
+import info.knacki.gitdroid.entities.GitPackableUtil;
+import info.knacki.gitdroid.io.OutputStreamWithCheckSum;
+
+class Pacman {
+    private static final Logger log = Logger.getLogger(Pacman.class.getName());
+    final Collection<GitPackable> fPacked;
+    final Map<GitPackable.eType, Collection<GitPackable>> fPackedByType;
+
+    public Pacman(Collection<GitPackable> objectsToPack) {
+        fPacked = objectsToPack;
+        fPackedByType = new HashMap<>();
+        for (GitPackable i: fPacked) {
+            Collection<GitPackable> col = fPackedByType.get(i.GetPackableType());
+            if (col == null) {
+                col = new ArrayDeque<>();
+                fPackedByType.put(i.GetPackableType(), col);
+            }
+            col.add(i);
+        }
+    }
+
+    private GitObject.GitTree FindTree(byte[] hash) {
+        Collection<GitPackable> col = fPackedByType.get(GitPackable.eType.eType_Tree);
+        if (col != null)
+            for (GitPackable i: col)
+                if (GitSha1.CompareChecksum(hash, GitSha1.getRawSha1OfPackable(i)))
+                    return (GitObject.GitTree) i;
+        return null;
+    }
+
+    private GitObject.GitRawData FindBlob(byte[] hash) {
+        Collection<GitPackable> col = fPackedByType.get(GitPackable.eType.eType_Blob);
+        if (col != null)
+            for (GitPackable i: col)
+                if (GitSha1.CompareChecksum(hash, GitSha1.getRawSha1OfPackable(i)))
+                    return (GitObject.GitRawData) i;
+        return null;
+    }
+
+    private void BuildTree(GitObject.GitTree root) throws PacmanBuilder.InvalidPackException {
+        GitObject.GitTree nextUninitialized = root;
+        do {
+            if (!nextUninitialized.IsRoot()) {
+                GitObject.GitTree realTree = FindTree(nextUninitialized.GetHash());
+                if (realTree == null)
+                    throw new PacmanBuilder.InvalidPackException("Cannot find object in pack");
+                realTree = new GitObject.GitTree(nextUninitialized.GetParent(), realTree, nextUninitialized.GetFilename());
+                nextUninitialized.GetParent().SetRealObject(nextUninitialized.GetFilename(), realTree);
+                nextUninitialized = realTree;
+            }
+            for (GitObject.GitBlob i: nextUninitialized.GetBlobs()) {
+                GitObject.GitRawData blob = FindBlob(i.GetHash());
+                if (blob == null)
+                    throw new PacmanBuilder.InvalidPackException("Cannot find object in pack");
+                nextUninitialized.SetRealObject(i.GetFilename(), new GitObject.GitRawData(nextUninitialized, i, blob));
+            }
+        } while ((nextUninitialized = root.FindNextNotInitializedTree()) != null);
+    }
+
+    private GitCommit BuildCommit(GitCommit ci) throws PacmanBuilder.InvalidPackException {
+        GitObject.GitTree tree = ci.GetTree();
+        if (tree == null) {
+            tree = FindTree(ci.GetTreeHash());
+            if (tree != null)
+                ci.SetTree(tree);
+        }
+        if (tree != null)
+            BuildTree(tree);
+        return ci;
+    }
+
+    public GitCommit BuildCommit(String hash) throws PacmanBuilder.InvalidPackException {
+        Collection<GitPackable> col = fPackedByType.get(GitPackable.eType.eType_Commit);
+        if (col != null)
+            for (GitPackable i: col)
+                if (hash.equals(((GitCommit) i).GetHash()))
+                    return BuildCommit((GitCommit) i);
+        return null;
+    }
+
+    public boolean Write(OutputStream out) throws IOException
+    {
+        OutputStreamWithCheckSum msg = new OutputStreamWithCheckSum(out);
+        msg.write("PACK")
+                .write(new byte[] { 0, 0, 0, 2 } )
+                .write(ByteBuffer.allocate(4).putInt(fPacked.size()).array());
+        for (GitPackable i: fPacked) {
+            byte[] pack = i.GetPack();
+            if (null == pack)
+                return false;
+            GitPackable.PackV2Header header = new GitPackable.PackV2Header(i.GetPackableType(), pack.length);
+            msg.write(GitPackableUtil.getObjHeader(header)).write(GitPackableUtil.deflate(pack));
+            log.info("Writing pack " +i.GetPackableType() +", " +GitSha1.getSha1OfPackable(i) +new String(i.GetPack()));
+        }
+        msg.writeSha1();
+        return true;
+    }
+}

+ 156 - 0
src/main/java/info/knacki/gitdroid/PacmanBuilder.java

@@ -0,0 +1,156 @@
+package info.knacki.gitdroid;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayDeque;
+import java.util.logging.Logger;
+import java.util.zip.DataFormatException;
+import java.util.zip.Inflater;
+
+import info.knacki.gitdroid.entities.GitPackable;
+import info.knacki.gitdroid.entities.GitPackableFactory;
+import info.knacki.gitdroid.entities.GitPackableUtil;
+
+public class PacmanBuilder {
+    private final static Logger log = Logger.getLogger(PacmanBuilder.class.getName());
+
+    private byte[] fBuffer;
+    private int fPos;
+
+    public static class InvalidPackException extends Throwable {
+        InvalidPackException(String msg) {
+            super(msg);
+        }
+
+        InvalidPackException(String msg, Throwable cause) {
+            super(msg, cause);
+        }
+    }
+
+    public Pacman build(byte[] data) throws InvalidPackException {
+        if (data == null)
+            throw new InvalidPackException("No data provided");
+        final String header = new String(data, 0, 4, Charset.defaultCharset());
+        if (!"PACK".equals(header))
+            throw new InvalidPackException("Invalid header " +header);
+
+        try {
+            MessageDigest checksum = MessageDigest.getInstance("SHA1");
+            checksum.update(data, 0, data.length - 20);
+            if (!GitSha1.CompareChecksum(checksum.digest(), data, data.length - 20))
+                throw new InvalidPackException("Checksum mismatch");
+        }
+        catch (NoSuchAlgorithmException e) {
+            throw new InvalidPackException("Cannot check pack integrity: " +e.getMessage(), e);
+        }
+
+        final int packVersion = ByteBuffer.wrap(data, 4, 4).getInt();
+        fBuffer = new byte[data.length -8 - 20];
+        fPos = 0;
+        System.arraycopy(data, 8, fBuffer, 0, fBuffer.length);
+
+        switch (packVersion) {
+            case 2:
+                return ReadPackV2();
+            default:
+                throw new InvalidPackException("Unsupported pack version " +packVersion);
+        }
+    }
+
+    private GitPackable.PackV2Header ReadPackV2Header() throws InvalidPackException {
+        int currentByte;
+        int nbBits = 4;
+
+        GitPackable.PackV2Header header = new GitPackable.PackV2Header();
+        currentByte = fBuffer[fPos++];
+        final int type = (currentByte & 0b01110000) >> 4;
+        header.type = GitPackableUtil.getObjType(type);
+        if (header.type == null)
+            throw new InvalidPackException("Unknown object type " +type +" in pack");
+        header.length = currentByte & 0b00001111;
+        while ((currentByte & 0b10000000) != 0) {
+            currentByte = fBuffer[fPos++];
+            header.length += (currentByte & 0b01111111) << nbBits;
+            nbBits += 7;
+        }
+        return header;
+    }
+
+    private byte[] InflateObject(GitPackable.PackV2Header header) throws InvalidPackException {
+        byte[] data = new byte[header.length];
+        try {
+            Inflater inf = new Inflater(false);
+            int totalRead = 0;
+            do {
+                if (inf.needsInput())
+                    inf.setInput(fBuffer, (int) (fPos +inf.getBytesRead()), fBuffer.length -fPos -(int)inf.getBytesRead());
+                totalRead += inf.inflate(data, totalRead, 1);
+            } while (!inf.finished() && totalRead < header.length);
+            fPos += inf.getBytesRead();
+        } catch (DataFormatException e) {
+            throw new InvalidPackException("Zip error: " + e.getMessage(), e);
+        }
+        return data;
+    }
+
+    private void InflateDiscard(int inflatedSize) throws InvalidPackException {
+        byte[] data = new byte[1];
+        try {
+            Inflater inf = new Inflater(false);
+            int totalRead = 0;
+            do {
+                if (inf.needsInput())
+                    inf.setInput(fBuffer, (int) (fPos +inf.getBytesRead()), fBuffer.length -fPos -(int)inf.getBytesRead());
+                totalRead += inf.inflate(data, 0, 1);
+            } while (!inf.finished() && totalRead < inflatedSize);
+            fPos += inf.getBytesRead();
+        } catch (DataFormatException e) {
+            throw new InvalidPackException("Zip error: " + e.getMessage(), e);
+        }
+    }
+
+    private Pacman ReadPackV2() throws InvalidPackException {
+        final int packCount = ByteBuffer.wrap(fBuffer, fPos, 4).getInt();
+        fPos += 4;
+        log.info("Found " +packCount +" object in pack file");
+        ArrayDeque<GitPackable> packed = new ArrayDeque<>();
+
+        for (int i =0; i < packCount; ++i) {
+            GitPackable.PackV2Header header = ReadPackV2Header();
+
+            switch (header.type) {
+                case eType_Blob:
+                case eType_Commit:
+                case eType_Tree:
+                case eType_Tag:
+                    byte[] inflated = InflateObject(header);
+                    String hash = GitSha1.GetSha1OfData(header.type, inflated);
+                    GitPackable pack = GitPackableFactory.build(header.type, hash, inflated);
+
+                    if (pack == null) {
+                        log.warning("Cannot parse object type " + header.type);
+                        continue;
+                    }
+                    packed.add(pack);
+                    log.info("Read " +GitSha1.getSha1OfPackable(pack) +" " + pack + " from pack");
+                    break;
+
+                case eType_RefDelta:
+                    log.info("Found ref_delta from base " +GitSha1.BytesToString(fBuffer, fPos));
+                    fPos += 20;
+                    InflateDiscard(header.length);
+                    break;
+
+                case eType_OfsDelta:
+                    //noinspection StatementWithEmptyBody
+                    while ((fBuffer[fPos++] & 0b10000000) != 0) {}
+                    InflateDiscard(header.length);
+                    break;
+            }
+        }
+        log.info("End reading pack, " +packed.size() +" object created");
+        return new Pacman(packed);
+    }
+}

+ 339 - 0
src/main/java/info/knacki/gitdroid/SSHGitProtocol.java

@@ -0,0 +1,339 @@
+package info.knacki.gitdroid;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.security.InvalidParameterException;
+import java.util.ArrayDeque;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import info.knacki.gitdroid.callback.OnErrorListener;
+import info.knacki.gitdroid.callback.OnResponseListener;
+import info.knacki.gitdroid.callback.OnStreamResponseListener;
+import info.knacki.gitdroid.entities.GitCommit;
+import info.knacki.gitdroid.entities.GitObject;
+import info.knacki.gitdroid.entities.GitRef;
+import info.knacki.gitdroid.io.AppendableInputStream;
+import info.knacki.gitdroid.io.ssh.SSHConnection;
+import info.knacki.gitdroid.io.ssh.SSHFactory;
+
+public class SSHGitProtocol extends BaseGitProtocol {
+    private final static Logger log = Logger.getLogger(SSHGitProtocol.class.getName());
+    private final SSHUrl fRepoUrl;
+    private GitRef[] fRefsCache = null;
+    private final class ActiveSSHWrapper {
+        final SSHConnection fConnection;
+        final OutputStream fStdin;
+        final InputStream fStdout;
+        final InputStream fStderr;
+
+        ActiveSSHWrapper(SSHConnection con, OutputStream stdin, InputStream stdout, InputStream stderr) {
+            fConnection = con;
+            fStdin = stdin;
+            fStdout = stdout;
+            fStderr = stderr;
+        }
+    }
+
+    private final class SSHUrl implements SSHConnection.ConnectionParams {
+        private final String repoHost;
+        final String repoName;
+
+        SSHUrl(String url) {
+            if (url.contains("://"))
+                url = url.substring(url.indexOf("://"));
+            int repoSep = url.indexOf(':');
+            if (repoSep < 0)
+                throw new InvalidParameterException("Cannot find repository name");
+            repoHost = url.substring(0, repoSep);
+            repoName = url.substring(repoSep + 1);
+        }
+
+        @Override
+        public String GetHostname() {
+            return repoHost;
+        }
+
+        public boolean HasAuthentication() {
+            return fConfig.HasAuthentication();
+        }
+
+        public String GetUser() {
+            return fConfig.GetUser();
+        }
+
+        public String GetPassword() {
+            return fConfig.GetPassword();
+        }
+
+        public String GetPrivateKey() {
+            return fConfig.GetPrivateKey();
+        }
+    }
+
+    SSHGitProtocol(GitConfig config) {
+        super(config);
+        fRepoUrl = new SSHUrl(config.GetUrl());
+    }
+
+    private byte[] ReadLine(InputStream in) throws IOException {
+        byte[] lineLenBytes = new byte[4];
+        if (in.read(lineLenBytes, 0, 4) < 4) {
+            return null;
+        }
+        int lineLen;
+        try {
+            lineLen = Integer.parseInt(new String(lineLenBytes, Charset.defaultCharset()), 16);
+        }
+        catch (NumberFormatException e) {
+            ByteArrayOutputStream stream = new ByteArrayOutputStream();
+            stream.write(lineLenBytes);
+            byte[] tmp = new byte[1024];
+            int read = in.read(tmp);
+            stream.write(tmp, 0, read);
+            String error = new String(stream.toByteArray(), Charset.defaultCharset());
+            throw new NumberFormatException(error);
+        }
+        if (lineLen == 0) {
+            return null;
+        }
+
+        lineLen -= 4;
+        if (lineLen == 0)
+            return new byte[] {};
+        byte[] line = new byte[lineLen];
+        if (in.read(line) != lineLen) {
+            return null;
+        }
+        for (int i =0; i < lineLen; ++i)
+            if (line[i] == 0)
+                lineLen = i;
+        return line;
+    }
+
+    private GitRef[] GetRefs(ActiveSSHWrapper sshConnection, OnErrorListener errorListener) {
+        ArrayDeque<GitRef> references = new ArrayDeque<>();
+
+        try {
+            byte[] line;
+            while ((line = ReadLine(sshConnection.fStdout)) != null) {
+                GitRef ref = new GitRef(new String(line, 0, 40, Charset.defaultCharset()), new String(line, 41, line.length -42, Charset.defaultCharset()));
+
+                if (ref.GetBranch().startsWith("refs/heads/")) {
+                    references.add(ref);
+                    log.info("Found ref: " + ref);
+                }
+            }
+        }
+        catch (IOException | NumberFormatException e) {
+            final String errorMsg = "Cannot read from ssh command: " +e.getMessage();
+            log.log(Level.SEVERE, errorMsg, e);
+            errorListener.OnError(errorMsg, e);
+            return null;
+        }
+        GitRef[] refs = new GitRef[references.size()];
+        return references.toArray(refs);
+    }
+
+    private void ListenForErrors(InputStream stderr, OnErrorListener onError) {
+        ListenForErrors(stderr, onError, true);
+    }
+
+    private void ListenForErrors(InputStream stderr, OnErrorListener onError, boolean separateThread) {
+        Thread thread = (new Thread() {
+            @Override
+            public void run() {
+                BufferedReader reader = new BufferedReader(new InputStreamReader(stderr));
+                String line;
+
+                try {
+                    while ((line = reader.readLine()) != null) {
+                        onError.OnError(line, null);
+                    }
+                }
+                catch (IOException e) {
+                    log.log(Level.SEVERE, "Cannot read from stderr", e);
+                }
+            }
+        });
+
+        if (separateThread)
+            thread.start();
+        else
+            thread.run();
+    }
+
+    @Override
+    public void GetRefs(OnResponseListener<GitRef[]> callback) {
+        if (fRefsCache != null) {
+            callback.OnResponse(fRefsCache);
+            return;
+        }
+        AppendableInputStream stdin = new AppendableInputStream();
+
+        SSHFactory
+                .createInstance(fRepoUrl, "git-upload-pack " + fRepoUrl.repoName)
+                .SetOnConnectionReadyListener((connection, stdout, stderr) -> {
+                    ListenForErrors(stderr, (msg, exc) -> log.severe(msg));
+                    fRefsCache = GetRefs(new ActiveSSHWrapper(connection, stdin.GetWriter(), stdout, stderr), callback);
+                    connection.disconnect();
+                    if (fRefsCache != null)
+                        callback.OnResponse(fRefsCache);
+                })
+                .connect();
+    }
+
+    private byte[] PullPackData(ActiveSSHWrapper sshWrapper, String hash, OnStreamResponseListener<byte[]> listener) {
+        try {
+            ListenForErrors(sshWrapper.fStderr, (msg, e) -> listener.OnMsg(msg));
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            out.write(GitLine("want " +hash));
+            out.write(GitLine("deepen 1"));
+            out.write(GitLine(null));
+            out.write(GitLine("done"));
+            sshWrapper.fStdin.write(out.toByteArray());
+            String line;
+            do {
+                byte[] b = ReadLine(sshWrapper.fStdout);
+                line = b == null ? null : new String(b, Charset.defaultCharset());
+            } while (!"NAK\n".equals(line));
+            return ReadAllStream(sshWrapper.fStdout);
+        }
+        catch (IOException e) {
+            log.log(Level.SEVERE, "Cannot get hash " +hash +": " +e.getMessage(), e);
+            return null;
+        }
+    }
+
+    private void PullPackData(String hash, OnStreamResponseListener<byte[]> response) {
+        AppendableInputStream stdin = new AppendableInputStream();
+
+        SSHFactory
+                .createInstance(fRepoUrl, "git-upload-pack " + fRepoUrl.repoName)
+                .SetOnConnectionReadyListener((connection, stdout, stderr) -> {
+                    ListenForErrors(stderr, (msg, exc) -> log.severe(msg));
+                    byte[] data = PullPackData(new ActiveSSHWrapper(connection, stdin.GetWriter(), stdout, stderr), hash, response);
+                    connection.disconnect();
+                    response.OnResponse(data);
+                })
+                .connect();
+    }
+
+    @Override
+    public void FetchCommit(GitRef headRef, OnStreamResponseListener<GitCommit> response) {
+        PullPackData(headRef.GetHash(), new OnStreamResponseListener<byte[]>() {
+            @Override
+            public void OnMsg(String message) {
+                response.OnMsg(message);
+            }
+
+            @Override
+            public void OnResponse(byte[] packData) {
+                try {
+                    Pacman pacman = new PacmanBuilder().build(packData);
+                    OnMsg("Read " +pacman.fPacked.size() +" objects from pack file");
+                    response.OnResponse(pacman.BuildCommit(headRef.GetHash()));
+                }
+                catch (PacmanBuilder.InvalidPackException e) {
+                    response.OnError("Cannot read pack object: " +e.getMessage(), e);
+                }
+            }
+
+            @Override
+            public void OnError(String msg, Throwable e) {
+                response.OnError(msg, e);
+            }
+        });
+    }
+
+    @Override
+    protected void PushPack(GitCommit commit, Pacman pack, String branch, OnStreamResponseListener<Void> resp) {
+        AppendableInputStream in = new AppendableInputStream();
+        SSHFactory
+            .createInstance(fRepoUrl, "git-receive-pack " +fRepoUrl.repoName)
+            .SetOnConnectionReadyListener((connection, stdout, stderr) -> {
+                ActiveSSHWrapper activeSSHWrapper = new ActiveSSHWrapper(connection, in.GetWriter(), stdout, stderr);
+                fRefsCache = null;
+                GitRef ref = GetRefFromBranchName(GetRefs(activeSSHWrapper, resp), branch);
+                ListenForErrors(stderr, (msg, e) -> resp.OnMsg(msg));
+                try {
+                    OutputStream w = in.GetWriter();
+                    String prev = ref == null ? "0000000000000000000000000000000000000000" : ref.GetHash();
+                    w.write(GitLine(prev + " " + GitSha1.BytesToString(GitSha1.getRawSha1OfPackable(commit)) + " " + branch));
+                    w.write(GitLine(null));
+                    if (!pack.Write(w)) {
+                        resp.OnError("Pack error", null);
+                        return;
+                    }
+                }
+                catch (IOException e) {
+                    resp.OnError("Cannot write pack: " +e.getMessage(), e);
+                }
+                ListenForErrors(stdout, (msg, e) -> resp.OnMsg(msg), false);
+                resp.OnResponse(null);
+            })
+            .connect(in);
+    }
+
+    @Override
+    public void FetchBlob(GitObject.GitBlob blob, OnStreamResponseListener<byte[]> response) {
+        if (blob instanceof GitObject.GitRawData)
+            response.OnResponse(((GitObject.GitRawData) blob).GetData());
+        else
+            response.OnError("Unsupported object " +new String(blob.GetHash(), Charset.defaultCharset()), new InvalidParameterException());
+    }
+
+    @Override
+    public void FetchHead(OnStreamResponseListener<GitCommit> response) {
+        AppendableInputStream stdin = new AppendableInputStream();
+
+        SSHFactory
+            .createInstance(fRepoUrl, "git-upload-pack " +fRepoUrl.repoName)
+            .SetOnConnectionReadyListener((connection, stdout, stderr) -> {
+                ActiveSSHWrapper SSHWrapper = new ActiveSSHWrapper(connection, stdin.GetWriter(), stdout, stderr);
+                GitRef ref = GetHeadRef(GetRefs(SSHWrapper, response));
+                if (ref == null)
+                    return;
+                GetHeadCommitFromRef(ref, new OnStreamResponseListener<GitCommit>() {
+                    @Override
+                    public void OnMsg(String message) {
+                        response.OnMsg(message);
+                    }
+
+                    @Override
+                    public void OnResponse(GitCommit result) {
+                        response.OnResponse(result);
+                    }
+
+                    @Override
+                    public void OnError(String msg, Throwable e) {
+                        response.OnError(msg, e);
+                    }
+                });
+            })
+            .connect(stdin);
+    }
+
+    private static byte[] ReadAllStream(InputStream s) throws IOException {
+        ByteArrayOutputStream str = new ByteArrayOutputStream();
+        int len;
+        byte[] buf = new byte[1024];
+        do {
+            len = s.read(buf, 0, 1024);
+            if (len > 0) {
+                str.write(buf, 0, len);
+            }
+        } while (len > 0);
+        return str.toByteArray();
+    }
+
+    @Override
+    protected void OnBranchCreated() {
+        fRefsCache = null;
+    }
+}

+ 5 - 0
src/main/java/info/knacki/gitdroid/callback/OnErrorListener.java

@@ -0,0 +1,5 @@
+package info.knacki.gitdroid.callback;
+
+public interface OnErrorListener {
+    void OnError(String msg, Throwable e);
+}

+ 5 - 0
src/main/java/info/knacki/gitdroid/callback/OnResponseListener.java

@@ -0,0 +1,5 @@
+package info.knacki.gitdroid.callback;
+
+public interface OnResponseListener<T> extends OnErrorListener {
+    void OnResponse(T result);
+}

+ 5 - 0
src/main/java/info/knacki/gitdroid/callback/OnStreamResponseListener.java

@@ -0,0 +1,5 @@
+package info.knacki.gitdroid.callback;
+
+public interface OnStreamResponseListener<T> extends OnResponseListener<T> {
+    void OnMsg(String message);
+}

+ 182 - 0
src/main/java/info/knacki/gitdroid/entities/GitCommit.java

@@ -0,0 +1,182 @@
+package info.knacki.gitdroid.entities;
+
+import android.support.annotation.NonNull;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import info.knacki.gitdroid.GitSha1;
+
+public class GitCommit implements GitPackable {
+    private final String fHash;
+    private byte[] fTreeHash;
+    private GitObject.GitTree fTree;
+    private String fParent;
+    private String fAuthor;
+    private String fCommitter;
+    private final String fMessage;
+    private Date fTime;
+
+    public GitCommit(String hash, String commitContent) {
+        int pos = commitContent.indexOf('\0');
+
+        if (pos >= 0)
+            commitContent = commitContent.substring(pos +1);
+        StringBuilder message = null;
+        for (String line: commitContent.split("\n")) {
+            if (message != null)
+                message.append(line).append('\n');
+            else if ("".equals(line))
+                message = new StringBuilder();
+            else if (line.startsWith("tree"))
+                fTreeHash = GitSha1.StringToBytes(Util.RemoveHead(line));
+            else if (line.startsWith("parent"))
+                fParent = Util.RemoveHead(line);
+            else if (line.startsWith("author"))
+                fAuthor = Util.RemoveHead(line);
+            else if (line.startsWith("committer"))
+                fCommitter = Util.RemoveHead(line);
+        }
+        fHash = hash;
+        fMessage = message == null ? "" : message.toString();
+        fTree = null;
+    }
+
+    protected GitCommit(GitCommit parent, String author, String committer, String msg) {
+        fAuthor = author;
+        fCommitter = committer;
+        fParent = parent != null ? parent.fHash : null;
+        fMessage = msg;
+        fHash = null;
+        fTime = new Date();
+    }
+
+    public String GetMessage() {
+        return fMessage;
+    }
+
+    public GitObject.GitTree GetTree() {
+        return fTree;
+    }
+
+    public byte[] GetTreeHash() { return fTreeHash; }
+
+    public GitObject.GitTree InitializeTree() {
+        return fTree = new GitObject.GitTree(GetTreeHash());
+    }
+
+    public String GetAuthor() {
+        return fAuthor;
+    }
+
+    public String GetCommitter() {
+        return fCommitter;
+    }
+
+    public String GetParent() {
+        return fParent;
+    }
+
+    public Date GetTime() {
+        return fTime;
+    }
+
+    @Override
+    public eType GetPackableType() {
+        return eType.eType_Commit;
+    }
+
+    @Override
+    public byte[] GetPack() {
+        String dateStr = "";
+        if (fTime != null)
+            dateStr = " " +(fTime.getTime() / 1000) +" " +(new SimpleDateFormat("Z", Locale.US).format(fTime));
+        String data = "tree " +(fTree == null ? GitSha1.BytesToString(fTreeHash) : GitSha1.BytesToString(fTree.GetHash())) +"\n" +
+                (GetParent() != null ? ("parent " +GetParent() +"\n") : "")+
+                "author " +GetAuthor() +dateStr +"\n" +
+                "committer " +GetCommitter() +dateStr +"\n" +
+                "\n" +
+                fMessage +"\n";
+        return data.getBytes();
+    }
+
+    public GitCommit SetTree(GitObject.GitTree tree) {
+        fTree = tree;
+        fTreeHash = GitSha1.getRawSha1OfPackable(tree);
+        return this;
+    }
+
+    public String GetHash() { return fHash; }
+
+    public static class Builder {
+        private final GitCommit co;
+        private final GitObject.GitTree fTree;
+        private final Set<String> fToPack = new TreeSet<>();
+
+        public Builder(GitCommit parent, String author, String authorEmail, String message) {
+            String aut = author +" <" +authorEmail +">";
+            co = new GitCommit(parent, aut, aut, message);
+            fTree = new GitObject.GitTree(null, parent.GetTree());
+        }
+
+        public Builder(String author, String authorEmail, String message) {
+            String aut = author +" <" +authorEmail +">";
+            co = new GitCommit(null, aut, aut, message);
+            fTree = GitObject.GitTree.CreateEmpty();
+        }
+
+        public GitCommit Build() {
+            co.fTree = fTree;
+            for (GitPackable go: PreparePack()) {
+                if (go instanceof GitObject.GitTree)
+                    ((GitObject.GitTree) go).GetHash();
+            }
+            return co;
+        }
+
+        public GitCommit.Builder AddFile(String relativeFilename, File f) {
+            if (f.exists()) {
+                GitObject obj = fTree.AddItem(relativeFilename, f);
+                do {
+                    fToPack.add(obj.GetGitPath());
+                    obj = obj.GetParent();
+                } while (obj != null);
+            } else {
+                GitObject obj = fTree.GetObjectFullPath(relativeFilename);
+                if (obj != null)
+                    do {
+                        obj.GetParent().Remove(obj.fName);
+                        fToPack.remove(obj.GetGitPath());
+                        obj = obj.GetParent();
+                    } while (obj != null && ((GitObject.GitTree) obj).fItems.size() == 0 && obj.GetParent() != null);
+                for (String i: fToPack)
+                    if (i.startsWith(relativeFilename +"/"))
+                        fToPack.remove(i);
+                while (obj != null) {
+                    fToPack.add(obj.GetGitPath());
+                    obj.fSha1 = null;
+                    obj = obj.GetParent();
+                }
+            }
+            return this;
+        }
+
+        public SortedSet<GitPackable> PreparePack() {
+            TreeSet<GitPackable> packs = new TreeSet<>(new GitPackableUtil.Comparator());
+            for (String i: fToPack)
+                packs.add("".equals(i) ? fTree : fTree.GetObjectFullPath(i));
+            packs.add(co);
+            return packs;
+        }
+    }
+
+    @Override @NonNull
+    public String toString() {
+        return "Commit " +fHash;
+    }
+}

+ 402 - 0
src/main/java/info/knacki/gitdroid/entities/GitObject.java

@@ -0,0 +1,402 @@
+package info.knacki.gitdroid.entities;
+
+import android.support.annotation.NonNull;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.ArrayDeque;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.logging.Logger;
+
+import info.knacki.gitdroid.GitSha1;
+
+public abstract class GitObject implements Comparable<GitObject>, GitPackable {
+    GitTree fParent;
+    final byte[] fMode;
+    final String fName;
+    byte[] fSha1;
+
+    @Override
+    public int compareTo(@NonNull GitObject gitObject) {
+        return fName.toLowerCase().compareTo(gitObject.fName.toLowerCase());
+    }
+
+    public static class GitBlob extends GitObject {
+        private File fFile;
+
+        private GitBlob(GitTree parent, byte[] mode, String name, byte[] sha1) {
+            super(parent, mode, name, sha1);
+            fFile = null;
+        }
+
+        private GitBlob(GitTree parent, GitBlob copy) {
+            this(parent, copy.fMode, copy.fName, copy.fSha1);
+        }
+
+        GitBlob(GitTree parent, String filename, File f) {
+            this(parent, new byte[] { 49, 48, 48, 54, 52, 52 }, filename, GitSha1.getRawSha1OfFile(f));
+            fFile = f;
+        }
+
+        @Override
+        public eType GetPackableType() {
+            return eType.eType_Blob;
+        }
+
+        public static byte[] GetFilePack(File f) throws IOException {
+            ByteArrayOutputStream str = new ByteArrayOutputStream();
+            int len;
+            byte[] buf = new byte[1024];
+            FileInputStream in = new FileInputStream(f);
+            do {
+                len = in.read(buf, 0, 1024);
+                if (len > 0) {
+                    str.write(buf, 0, len);
+                }
+            } while (len > 0);
+            return str.toByteArray();
+        }
+
+        @Override
+        public byte[] GetPack() {
+            try
+            {
+                return GetFilePack(fFile);
+            }
+            catch (FileNotFoundException e) {
+                return null;
+            }
+            catch(IOException t) {
+                t.printStackTrace();
+            }
+            return null;
+        }
+    }
+
+    public static class GitRawData extends GitBlob {
+        private final byte[] fData;
+
+        GitRawData(byte[] sha1, byte[] data) {
+            super(null, new byte[] { 49, 48, 48, 54, 52, 52 }, "", sha1);
+            fData = data;
+        }
+
+        public GitRawData(GitTree parent, byte[] mode, String name, byte[] sha1) {
+            super(parent, mode, name, sha1);
+            fData = null;
+        }
+
+        public GitRawData(GitTree parent, GitBlob emptyBlob, GitRawData prev) {
+            super(parent, new byte[] { 49, 48, 48, 54, 52, 52 }, emptyBlob.GetFilename(), prev.GetHash());
+            fData = prev.fData;
+        }
+
+        @Override
+        public eType GetPackableType() {
+            return eType.eType_Blob;
+        }
+
+        @Override
+        public byte[] GetPack() {
+            return GetData();
+        }
+
+        public byte[] GetData() { return fData; }
+    }
+
+    public static class GitTree extends GitObject {
+        HashMap<String, GitObject> fItems = null;
+        ArrayDeque<String> fItemOrder;
+
+        private GitTree(GitTree parent, byte[] mode, String name, byte[] sha1) {
+            super(parent, mode, name, sha1);
+        }
+
+        GitTree(GitTree parent, GitTree copy) {
+            this(parent, copy.fMode, copy.fName, copy.fSha1);
+            Initialize();
+            for (Map.Entry<String, GitObject> o: copy.fItems.entrySet()) {
+                GitObject src = o.getValue();
+                GitObject oCopy = src instanceof GitTree ? new GitTree(this, (GitTree) src) : new GitBlob(this, (GitBlob) src);
+                fItems.put(o.getKey(), oCopy);
+            }
+            for (String i: copy.fItemOrder)
+                fItemOrder.push(i);
+        }
+
+        GitTree(byte[] sha1) {
+            super(null, new byte[] { '4', '0', '0', '0', '0' }, "", sha1);
+        }
+
+        private GitTree(GitTree parent, String filename) {
+            super(parent, new byte[] { '4', '0', '0', '0', '0' }, filename, null);
+        }
+
+        public GitTree(GitTree parent, GitTree content, String filename) {
+            this(parent, filename);
+            fItems = content.fItems;
+            fItemOrder = content.fItemOrder;
+            for (GitObject i: fItems.values()) {
+                i.SetParent(this);
+            }
+        }
+
+        public static GitTree CreateEmpty() {
+            GitTree tree = new GitTree(null);
+            return tree.Initialize();
+        }
+
+        public GitTree Initialize() {
+            fItems = new HashMap<>();
+            fItemOrder = new ArrayDeque<>();
+            return this;
+        }
+
+        boolean IsInitialized() {
+            return null != fItems;
+        }
+
+        public GitObject GetObject(String name) {
+            return fItems.get(name);
+        }
+
+        public GitObject GetObjectFullPath(String path) {
+            if ('/' == path.charAt(0))
+                path = path.substring(1);
+            final int nextSlash = path.indexOf('/');
+            final String filename = nextSlash == -1 ? path : (path.substring(0, nextSlash));
+            final GitObject obj = GetObject(filename);
+            if (obj == null || (obj instanceof GitBlob && nextSlash != -1))
+                return null;
+            if (nextSlash == -1)
+                return obj;
+            return ((GitTree) obj).GetObjectFullPath(path.substring(nextSlash +1));
+        }
+
+        public GitTree AddItem(GitObject item) {
+            fItems.put(item.fName, item);
+            fItemOrder.push(item.fName);
+            return this;
+        }
+
+        public GitTree FindNextNotInitializedTree() {
+            if (!IsInitialized())
+                return this;
+            for (GitObject obj: fItems.values()) {
+                if (!(obj instanceof GitTree))
+                    continue;
+                GitTree child = ((GitTree) obj).FindNextNotInitializedTree();
+                if (child != null)
+                    return child;
+            }
+            return null;
+        }
+
+        public Iterable<GitObject> GetObjects() {
+            ArrayDeque<GitObject> objects = new ArrayDeque<>();
+            for (String i: fItemOrder)
+                objects.push(fItems.get(i));
+            return objects;
+        }
+
+        public Iterable<GitBlob> GetBlobs() {
+            SortedSet<GitBlob> objects = new TreeSet<>();
+            for (GitObject i: fItems.values())
+                if (i instanceof GitBlob)
+                    objects.add((GitBlob) i);
+            return objects;
+        }
+
+        public Iterable<GitTree> GetTrees() {
+            SortedSet<GitTree> objects = new TreeSet<>();
+            for (GitObject i: fItems.values())
+                if (i instanceof GitTree)
+                    objects.add((GitTree) i);
+            return objects;
+        }
+
+        public String GetPath() {
+            if (fParent != null)
+                return fParent.GetPath() +GetFilename() +"/";
+            return GetFilename() +"/";
+        }
+
+        public void SetRealObject(String key, GitObject obj) {
+            if (fItems.containsKey(key)) {
+                fItems.put(key, obj);
+            }
+        }
+
+        private void FindAllBlobs(HashMap<String, GitBlob> data) {
+            String rootPath = GetPath();
+            for (GitBlob i: GetBlobs())
+                data.put(rootPath +i.GetFilename(), i);
+            for (GitTree i: GetTrees())
+                i.FindAllBlobs(data);
+        }
+
+        public HashMap<String, GitBlob> FindAllBlobs() {
+            HashMap<String, GitBlob> result = new HashMap<>();
+            FindAllBlobs(result);
+            return result;
+        }
+
+        public boolean IsRoot() {
+            return fParent == null;
+        }
+
+        public GitObject AddItem(String path, File f) {
+            if ('/' == path.charAt(0))
+                path = path.substring(1);
+            final int nextSlash = path.indexOf('/');
+            final String filename = nextSlash == -1 ? path : (path.substring(0, nextSlash));
+            final GitObject obj = GetObject(filename);
+            final GitObject newItem;
+
+            if (obj == null) {
+                if (nextSlash != -1) {
+                    GitTree child = new GitTree(this, filename).Initialize();
+                    newItem = child.AddItem(path.substring(nextSlash + 1), f);
+                    fItems.put(filename, child);
+                } else {
+                    newItem = new GitBlob(this, filename, f);
+                    fItems.put(filename, newItem);
+                }
+                fItemOrder.push(filename);
+            } else if (nextSlash == -1) {
+                Remove(filename);
+                newItem = new GitBlob(this, filename, f);
+                fItems.put(filename, newItem);
+                fItemOrder.push(filename);
+            } else {
+                newItem = ((GitTree) obj).AddItem(path.substring(nextSlash + 1), f);
+            }
+            fSha1 = null;
+            return newItem;
+        }
+
+        public GitTree Remove(String filename) {
+            fItems.remove(filename);
+            fItemOrder.remove(filename);
+            fSha1 = null;
+            return this;
+        }
+
+        public void Fill(byte[] data) {
+            int i = 0;
+
+            while (i < data.length) {
+                byte[] mode = null;
+                int len;
+                for (len =0; i +len <data.length && len < 8; ++len) {
+                    if (data[i + len] == ' ') {
+                        mode = new byte[len];
+                        System.arraycopy(data, i, mode, 0, len);
+                        i += len + 1;
+                        break;
+                    }
+                }
+                if (mode == null)
+                    break;
+                len =0;
+                while (i +len < data.length && data[i +len] != 0)
+                    ++len;
+                byte[] filenameBytes = new byte[len];
+                System.arraycopy(data, i, filenameBytes, 0, len);
+                i += len +1;
+                if (i +20 > data.length)
+                    break;
+                byte []sha1 = new byte[20];
+                System.arraycopy(data, i, sha1, 0, 20);
+                i += 20;
+                AddItem(GitObject.factory(this, mode, new String(filenameBytes, Charset.defaultCharset()), sha1));
+            }
+        }
+
+        @Override
+        public eType GetPackableType() {
+            return eType.eType_Tree;
+        }
+
+        @Override
+        public byte[] GetPack() {
+            try {
+                ByteArrayOutputStream str = new ByteArrayOutputStream();
+
+                for (GitObject go : GetObjects()) {
+                    str.write((go.GetMode() + ' ').getBytes());
+                    if (go.fName.equals("Aa") || "Oo".equals(go.fName))
+                        Logger.getLogger(GitObject.class.getName()).severe("out file in tree " +go.GetGitPath() +"$" + GitSha1.BytesToString(go.GetHash()));
+                    str.write(go.GetFilename().getBytes());
+                    str.write(new byte[]{0});
+                    str.write(go.GetHash());
+                }
+                return str.toByteArray();
+            }
+            catch (IOException e) {
+                return null;
+            }
+        }
+
+        @Override
+        public byte[] GetHash() {
+            if (null != fSha1)
+                return fSha1;
+            return fSha1 = GitSha1.getRawSha1OfPackable(this);
+        }
+    }
+
+    private GitObject(GitTree parent, byte[] mode, String name, byte[] sha1) {
+        fParent = parent;
+        fMode = mode;
+        fName = name;
+        fSha1 = sha1;
+    }
+
+    public static GitObject factory(GitTree parent, byte[] mode, String name, byte[] sha1) {
+        if (mode[0] == '4') {
+            return new GitTree(parent, mode, name, sha1);
+        }
+        return new GitRawData(parent, mode, name, sha1);
+    }
+
+    public byte[] GetHash() {
+        return fSha1;
+    }
+
+    public String GetFilename() {
+        return fName;
+    }
+
+    public String GetGitPath() {
+        StringBuilder sb = new StringBuilder();
+        GitTree parent = fParent;
+        sb.insert(0, fName);
+        while (parent != null) {
+            sb.insert(0, parent.fName + "/");
+            parent = parent.fParent;
+        }
+        return sb.toString();
+    }
+
+    public GitTree GetParent() {
+        return fParent;
+    }
+
+    private void SetParent(GitTree t) { fParent = t; }
+
+    public String GetMode() {
+        return new String(fMode);
+    }
+
+    public int GetDepth() {
+        return fParent == null ? 1 : fParent.GetDepth() +1;
+    }
+}

+ 29 - 0
src/main/java/info/knacki/gitdroid/entities/GitPackable.java

@@ -0,0 +1,29 @@
+package info.knacki.gitdroid.entities;
+
+public interface GitPackable {
+    enum eType {
+        eType_Commit,
+        eType_Tree,
+        eType_Blob,
+        eType_Tag,
+        eType_OfsDelta,
+        eType_RefDelta
+    }
+
+    class PackV2Header {
+        public eType type;
+        public int length;
+
+        public PackV2Header() {
+            this(null, 0);
+        }
+
+        public PackV2Header(eType type, int length) {
+            this.type = type;
+            this.length = length;
+        }
+    }
+
+    eType GetPackableType();
+    byte[] GetPack();
+}

+ 28 - 0
src/main/java/info/knacki/gitdroid/entities/GitPackableFactory.java

@@ -0,0 +1,28 @@
+package info.knacki.gitdroid.entities;
+
+import java.nio.charset.Charset;
+
+import info.knacki.gitdroid.GitSha1;
+
+public class GitPackableFactory {
+    public static GitPackable build(GitPackable.eType type, String hash, byte[] data) {
+        switch (type) {
+            case eType_Commit:
+                return new GitCommit(hash, new String(data, Charset.defaultCharset()));
+
+            case eType_Tree:
+                GitObject.GitTree tree = new GitObject.GitTree(GitSha1.StringToBytes(hash));
+                tree.Initialize().Fill(data);
+                return tree;
+
+            case eType_Blob:
+                return new GitObject.GitRawData(GitSha1.StringToBytes(hash), data);
+
+            case eType_OfsDelta:
+            case eType_RefDelta:
+            case eType_Tag:
+                break;
+        }
+        return null;
+    }
+}

+ 112 - 0
src/main/java/info/knacki/gitdroid/entities/GitPackableUtil.java

@@ -0,0 +1,112 @@
+package info.knacki.gitdroid.entities;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.zip.DeflaterOutputStream;
+
+public class GitPackableUtil {
+    public static byte getObjType(GitPackable.eType type) {
+        switch (type) {
+            case eType_Commit:
+                return 1;
+            case eType_Tree:
+                return 2;
+            case eType_Blob:
+                return 3;
+            case eType_Tag:
+                return 4;
+            case eType_OfsDelta:
+                return 6;
+            case eType_RefDelta:
+                return 7;
+        }
+        return -1;
+    }
+
+    public static GitPackable.eType getObjType(int type) {
+        switch (type) {
+            case 1:
+                return GitPackable.eType.eType_Commit;
+            case 2:
+                return GitPackable.eType.eType_Tree;
+            case 3:
+                return GitPackable.eType.eType_Blob;
+            case 4:
+                return GitPackable.eType.eType_Tag;
+            case 6:
+                return GitPackable.eType.eType_OfsDelta;
+            case 7:
+                return GitPackable.eType.eType_RefDelta;
+        }
+        return null;
+    }
+
+    public static String GetObjTypeString(GitPackable.eType type) {
+        switch (type) {
+            case eType_Commit:
+                return "commit";
+            case eType_Tree:
+                return "tree";
+            case eType_Blob:
+                return "blob";
+        }
+        return "";
+    }
+
+    private static int bitToWrite(long len)
+    {
+        int nbBits = 0;
+        while (len > 0)
+        {
+            nbBits++;
+            len = len >> 1;
+        }
+        int remains = (nbBits -4) % 7;
+        if (remains > 0)
+            nbBits += 7 -remains;
+        return nbBits;
+    }
+
+    public static byte[] getObjHeader(GitPackable.PackV2Header header) {
+        int len = header.length;
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        byte lastByte;
+        lastByte = (byte) ((getObjType(header.type)) << 4);
+        int nbBitToWrite = bitToWrite(len);
+        nbBitToWrite -= 4;
+        lastByte |= (byte) (len & 0b1111);
+        len = len >> 4;
+        while (nbBitToWrite > 0) {
+            lastByte |= 0b10000000;
+            buffer.write(lastByte);
+            lastByte = 0;
+            nbBitToWrite -= 7;
+            lastByte |= (byte)(len & 0b1111111);
+            len = len >> 7;
+        }
+        buffer.write(lastByte);
+        return buffer.toByteArray();
+    }
+
+    public static byte[] deflate(byte[] in) throws IOException {
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        DeflaterOutputStream stream = new DeflaterOutputStream(output);
+        stream.write(in);
+        stream.finish();
+        stream.flush();
+        return output.toByteArray();
+    }
+
+    public static class Comparator implements java.util.Comparator<GitPackable> {
+        @Override
+        public int compare(GitPackable t1, GitPackable t2) {
+            if (t1 instanceof GitObject && t2 instanceof GitObject)
+                return ((GitObject) t2).GetDepth() - ((GitObject) t1).GetDepth();
+            if (t1 instanceof GitObject /* then !(t2 instanceof GitObject) */)
+                return -1;
+            if (t2 instanceof GitObject /* then !(t1 instanceof GitObject) */)
+                return 1;
+            return (int)(((GitCommit)t1).GetTime().getTime() - ((GitCommit) t2).GetTime().getTime());
+        }
+    }
+}

+ 38 - 0
src/main/java/info/knacki/gitdroid/entities/GitRef.java

@@ -0,0 +1,38 @@
+package info.knacki.gitdroid.entities;
+
+import android.support.annotation.NonNull;
+
+public class GitRef {
+    protected final String fHash;
+    protected final String fBranch;
+
+    public GitRef(String hash, String branch) {
+        fHash = hash;
+        int pos = branch.indexOf('\0');
+        if (pos >= 0)
+            fBranch = branch.substring(0, pos);
+        else
+            fBranch = branch;
+    }
+
+    public String GetBranch() {
+        return fBranch;
+    }
+
+    public String GetHash() {
+        return fHash;
+    }
+
+    public String GetBranchName() {
+        int branchIndex = fBranch.lastIndexOf('/');
+        if (branchIndex == -1)
+            return fBranch;
+        return fBranch.substring(branchIndex + 1);
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "ref {" + fHash + "} for branch {" + fBranch + "}";
+    }
+}

+ 10 - 0
src/main/java/info/knacki/gitdroid/entities/Util.java

@@ -0,0 +1,10 @@
+package info.knacki.gitdroid.entities;
+
+public class Util {
+    public static String RemoveHead(String in) {
+        int pos = in.indexOf(' ');
+        if (pos >= 0)
+            return in.substring(pos +1);
+        return in;
+    }
+}

+ 64 - 0
src/main/java/info/knacki/gitdroid/io/AppendableInputStream.java

@@ -0,0 +1,64 @@
+package info.knacki.gitdroid.io;
+
+import org.bouncycastle.crypto.tls.ByteQueue;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class AppendableInputStream extends InputStream {
+    private boolean fClosed;
+    private final ByteQueue buffer;
+    private final Writer fWriter;
+
+    public class Writer extends OutputStream {
+        @Override
+        public void write(int b) {
+            buffer.addData(new byte[] {(byte) (b & 0xFF)}, 0, 1);
+        }
+
+        public void close() {
+            fClosed = true;
+        }
+    }
+
+    public AppendableInputStream() {
+        buffer = new ByteQueue(256);
+        fWriter = this.new Writer();
+        fClosed = false;
+    }
+
+    public Writer GetWriter() {
+        return fWriter;
+    }
+
+    @Override
+    public int read() {
+        if (buffer.available() > 0)
+            return buffer.removeData(1, 0)[0] & 0xFF;
+        return -1;
+    }
+
+    @Override
+    public int read(byte[] b) {
+        return read(b, 0, b.length);
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) {
+        if (fClosed)
+            return 0;
+        int maxRead = Math.min(len, available());
+        buffer.removeData(b, off, maxRead, 0);
+        return maxRead;
+    }
+
+    @Override
+    public int available() {
+        return fClosed ? 0 : buffer.available();
+    }
+
+    @Override
+    public void close() {
+        fWriter.close();
+    }
+}

+ 242 - 0
src/main/java/info/knacki/gitdroid/io/NetworkUtils.java

@@ -0,0 +1,242 @@
+package info.knacki.gitdroid.io;
+
+import android.os.AsyncTask;
+import android.util.Base64;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Authenticator;
+import java.net.HttpURLConnection;
+import java.net.PasswordAuthentication;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.zip.InflaterInputStream;
+
+import info.knacki.gitdroid.callback.OnResponseListener;
+
+public class NetworkUtils {
+    private final static Logger log = Logger.getLogger(NetworkUtils.class.getName());
+
+    public interface AuthConfig {
+        boolean HasAuthentication();
+        String GetUser();
+        String GetPassword();
+        String GetPrivateKey();
+    }
+
+    static class DownloaderTask extends AsyncTask<Void, Void, Integer> {
+        private final URL fUrl;
+        private OnResponseListener<byte[]> fResp;
+        private final AuthConfig fConfig;
+
+        DownloaderTask(URL url, AuthConfig config, OnResponseListener<byte[]> resp) {
+            fUrl = url;
+            fResp = resp;
+            fConfig = config;
+        }
+
+        DownloaderTask(URL url, AuthConfig config) {
+            fUrl = url;
+            fConfig = config;
+        }
+
+        void SetResultHandler(OnResponseListener<byte[]> handler) {
+            fResp = handler;
+        }
+
+        protected InputStream GetInputFilter(URLConnection in) throws IOException {
+            return in.getInputStream();
+        }
+
+        protected void ManageResponse(byte[] resp) {
+            fResp.OnResponse(resp);
+        }
+
+        @Override
+        protected Integer doInBackground(Void... voids) {
+            try {
+                log.log(Level.INFO, "fetching " +fUrl.toString());
+                URLConnection httpClient = fUrl.openConnection();
+                if (fConfig.HasAuthentication()) {
+                    Authenticator.setDefault(new Authenticator() {
+                        @Override
+                        protected PasswordAuthentication getPasswordAuthentication() {
+                            return new PasswordAuthentication(fConfig.GetUser(), fConfig.GetPassword().toCharArray());
+                        }
+                    });
+                }
+                InputStream in = GetInputFilter(httpClient);
+                ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+
+                int len;
+                byte[] buf = new byte[1024];
+                do {
+                    len = in.read(buf, 0, 1024);
+                    if (len > 0) {
+                        buffer.write(buf, 0, len);
+                    }
+                } while (len > 0);
+                in.close();
+                ManageResponse(buffer.toByteArray());
+            }
+            catch (IOException e) {
+                log.log(Level.WARNING, e.getMessage(), e);
+                fResp.OnError(e.getClass().getSimpleName() +": " +e.getMessage(), e);
+            }
+            return 0;
+        }
+    }
+
+    static class PostTask extends AsyncTask<Void, Void, Integer> {
+        private final URL fUrl;
+        private final OnResponseListener<byte[]> fResp;
+        private final AuthConfig fConfig;
+        private final OnResponseListener<OutputStream> fOnReadyWrite;
+        private final Map<String, String> fHeaders;
+
+        PostTask(URL url, AuthConfig config, Map<String, String> headers, OnResponseListener<byte[]> resp, OnResponseListener<OutputStream> onReadyWrite) {
+            fUrl = url;
+            fResp = resp;
+            fConfig = config;
+            fOnReadyWrite = onReadyWrite;
+            fHeaders = headers;
+        }
+
+        protected InputStream GetInputFilter(URLConnection in) throws IOException {
+            try {
+                return in.getInputStream();
+            } catch (Throwable e) {
+                Map<String, List<String>> headers = in.getHeaderFields();
+                String msg = null;
+                if (!headers.values().isEmpty()) {
+                    List<String> statuses = headers.values().iterator().next();
+                    if (!statuses.isEmpty())
+                        msg = in.getURL().toString() + ": " +statuses.iterator().next();
+                }
+                if (msg == null)
+                    msg = "Tried to reach " +in.getURL().toString();
+                log.log(Level.SEVERE, msg, e);
+                throw e;
+            }
+        }
+
+        protected void ManageResponse(byte[] resp) {
+            fResp.OnResponse(resp);
+        }
+
+        @Override
+        protected Integer doInBackground(Void... voids) {
+            try {
+                log.log(Level.INFO, "fetching " +fUrl.toString());
+                HttpURLConnection httpClient = (HttpURLConnection) fUrl.openConnection();
+                httpClient.setRequestMethod("POST");
+                for (Map.Entry<String, String> header: fHeaders.entrySet()) {
+                    httpClient.setRequestProperty(header.getKey(), header.getValue());
+                }
+                if (fConfig.HasAuthentication()) {
+                    httpClient.setRequestProperty("Authorization", "basic " +Base64.encodeToString("fConfig.GetUser():fConfig.GetPassword()".getBytes(), Base64.NO_WRAP));
+                }
+                fOnReadyWrite.OnResponse(httpClient.getOutputStream());
+                InputStream in = GetInputFilter(httpClient);
+                ArrayList<byte[]> fullBuffer = new ArrayList<>();
+                int totalRead = 0;
+                byte[] buffer = new byte[1024];
+                int currentRead;
+                while ((currentRead = in.read(buffer, 0, 1024)) > 0) {
+                    fullBuffer.add(buffer);
+                    buffer = new byte[1024];
+                    totalRead += currentRead;
+                    if (currentRead < 1024)
+                        break;
+                }
+                in.close();
+                if (totalRead >= 0) {
+                    buffer = new byte[totalRead];
+                    int i = 0;
+                    for (byte[] currentBuf : fullBuffer) {
+                        for (int j = 0; j < currentBuf.length && i * 1024 + j < totalRead; ++j) {
+                            buffer[i * 1024 + j] = currentBuf[j];
+                        }
+                        ++i;
+                    }
+                    ManageResponse(buffer);
+                } else {
+                    ManageResponse(new byte[] {});
+                }
+            }
+            catch (IOException e) {
+                log.log(Level.WARNING, e.getMessage(), e);
+                fResp.OnError(e.getClass().getSimpleName() +": " +e.getMessage(), e);
+            }
+            return 0;
+        }
+    }
+
+    static class DownloaderWithInflaterTask extends DownloaderTask {
+        DownloaderWithInflaterTask(URL url, AuthConfig config, OnResponseListener<byte[]> resp) {
+            super(url, config, resp);
+        }
+
+        @Override
+        protected InputStream GetInputFilter(URLConnection in) throws IOException {
+            return new InflaterInputStream(super.GetInputFilter(in));
+        }
+
+        @Override
+        protected void ManageResponse(byte[] result) {
+            int i =0;
+
+            while (i < result.length && result[i] != 0)
+                ++i;
+
+            if (i != result.length) {
+                ++i;
+                byte []arr = new byte[result.length -i];
+                System.arraycopy(result, i, arr, 0, result.length -i);
+                super.ManageResponse(arr);
+            } else {
+                super.ManageResponse(result);
+            }
+        }
+    }
+
+    static class StringDownloaderTask extends DownloaderTask {
+        private final OnResponseListener<String> onResp;
+
+        StringDownloaderTask(URL url, AuthConfig config, OnResponseListener<String> resultHandler) {
+            super(url, config);
+            SetResultHandler(new OnResponseListener<byte[]>() {
+                @Override
+                public void OnResponse(byte[] result) {
+                    StringDownloaderTask.this.onResp.OnResponse(new String(result, Charset.defaultCharset()));
+                }
+
+                @Override
+                public void OnError(String msg, Throwable e) {
+                    StringDownloaderTask.this.onResp.OnError(msg, e);
+                }
+            });
+            onResp = resultHandler;
+        }
+    }
+
+    public static void protoInflateGet(final URL url, AuthConfig config, final OnResponseListener<byte[]> callback) {
+        new DownloaderWithInflaterTask(url, config, callback).execute();
+    }
+
+    public static void ProtoPost(URL url, Map<String, String> headers, AuthConfig config, OnResponseListener<byte[]> callback, OnResponseListener<OutputStream> onReadyWrite) {
+        new PostTask(url, config, headers, callback, onReadyWrite).execute();
+    }
+
+    public static void protoGet(final URL url, AuthConfig config, final OnResponseListener<String> callback) {
+        new StringDownloaderTask(url, config, callback).execute();
+    }
+}

+ 41 - 0
src/main/java/info/knacki/gitdroid/io/OutputStreamWithCheckSum.java

@@ -0,0 +1,41 @@
+package info.knacki.gitdroid.io;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class OutputStreamWithCheckSum {
+    private static final Logger log = Logger.getLogger(OutputStreamWithCheckSum.class.getName());
+    private final OutputStream fOutput;
+    private MessageDigest fDigest;
+
+    public OutputStreamWithCheckSum(OutputStream out) {
+        fOutput = out;
+        try {
+            fDigest = MessageDigest.getInstance("SHA1");
+        }
+        catch (NoSuchAlgorithmException e) {
+            log.log(Level.SEVERE, e.getMessage(), e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public OutputStreamWithCheckSum write(byte []b) throws IOException {
+        fDigest.update(b);
+        fOutput.write(b);
+        return this;
+    }
+
+    public OutputStreamWithCheckSum write(String b) throws IOException {
+        return write(b.getBytes(Charset.defaultCharset()));
+    }
+
+    public OutputStreamWithCheckSum writeSha1() throws IOException {
+        fOutput.write(fDigest.digest());
+        return this;
+    }
+}

+ 135 - 0
src/main/java/info/knacki/gitdroid/io/ssh/JSchWrapper.java

@@ -0,0 +1,135 @@
+package info.knacki.gitdroid.io.ssh;
+
+import android.os.AsyncTask;
+
+import com.jcraft.jsch.ChannelExec;
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.JSchException;
+import com.jcraft.jsch.Session;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.security.InvalidParameterException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class JSchWrapper extends AsyncTask<InputStream, Void, Void> implements SSHConnection {
+    private static final Logger log = Logger.getLogger(JSchWrapper.class.getName());
+    private final String fCommand;
+    private final ConnectionParams fAuth;
+    private OnConnectionReadyListener fReadyListener;
+
+    private final PipedOutputStream stdout_out = new PipedOutputStream();
+    private final PipedOutputStream stderr_out = new PipedOutputStream();
+    private final PipedInputStream stdout = new PipedInputStream();
+    private final PipedInputStream stderr = new PipedInputStream();
+
+    private ChannelExec fChannel = null;
+
+    public JSchWrapper(ConnectionParams config, String command) {
+        fCommand = command;
+        fAuth = config;
+    }
+
+    @Override
+    public SSHConnection SetOnConnectionReadyListener(OnConnectionReadyListener listener) {
+        fReadyListener = listener;
+        return this;
+    }
+
+    @Override
+    public byte[] GetOutputStd() throws IOException {
+        return ReadAllStream(stdout);
+    }
+
+    @Override
+    public byte[] GetOutputErr() throws IOException {
+        return ReadAllStream(stderr);
+    }
+
+    private byte[] ReadAllStream(InputStream in) throws IOException {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        int len;
+        byte[] buf = new byte[1024];
+        do {
+            len = in.read(buf, 0, 1024);
+            if (len > 0) {
+                out.write(buf, 0, len);
+            }
+        } while (len > 0);
+        return out.toByteArray();
+    }
+
+    @Override
+    public SSHConnection connect() {
+        if (fChannel != null)
+            throw new InvalidParameterException("JSch Already connected");
+        execute();
+        return this;
+    }
+
+    @Override
+    public SSHConnection connect(InputStream stdin) {
+        if (fChannel != null)
+            throw new InvalidParameterException("JSch Already connected");
+        execute(stdin);
+        return this;
+    }
+
+    @Override
+    protected Void doInBackground(InputStream... in) {
+        InitConnection(in == null || in.length == 0 ? null : in[0]);
+        if (fChannel != null) {
+            try {
+                stderr.connect(stderr_out);
+                stdout.connect(stdout_out);
+            }
+            catch (IOException e) {
+                log.log(Level.SEVERE, "Cannot read from output: " +e.getMessage(), e);
+            }
+            if (fReadyListener != null)
+                fReadyListener.OnConnectionReady(this, stdout, stderr);
+        }
+        return null;
+    }
+
+    private void InitConnection(InputStream in) {
+        try {
+            // Connection settings
+            JSch connection = new JSch();
+            String privateKey = fAuth.GetPrivateKey();
+            Session session = connection.getSession(fAuth.GetUser(), fAuth.GetHostname());
+            session.setConfig("StrictHostKeyChecking", "no"); // FIXME
+            if (privateKey != null && !privateKey.isEmpty()) {
+                connection.addIdentity(privateKey); // FIXME key passphrase
+            } else {
+                session.setPassword(fAuth.GetPassword());
+            }
+            session.connect();
+            fChannel = (ChannelExec) session.openChannel("exec");
+
+            // Stream settings
+            if (in != null)
+                fChannel.setInputStream(in);
+            fChannel.setErrStream(stderr_out);
+            fChannel.setOutputStream(stdout_out);
+
+            // do connect
+            fChannel.setCommand(fCommand);
+            fChannel.connect();
+        }
+        catch (JSchException e) {
+            log.log(Level.SEVERE, "Cannot Initiate ssh command: " +e.getMessage(), e);
+            fChannel = null;
+        }
+    }
+
+    @Override
+    public void disconnect() {
+        fChannel.disconnect();
+        fChannel = null;
+    }
+}

+ 23 - 0
src/main/java/info/knacki/gitdroid/io/ssh/SSHConnection.java

@@ -0,0 +1,23 @@
+package info.knacki.gitdroid.io.ssh;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import info.knacki.gitdroid.io.NetworkUtils;
+
+public interface SSHConnection {
+    interface OnConnectionReadyListener {
+        void OnConnectionReady(SSHConnection connection, InputStream stdout, InputStream stderr);
+    }
+
+    interface ConnectionParams extends NetworkUtils.AuthConfig {
+        String GetHostname();
+    }
+
+    SSHConnection SetOnConnectionReadyListener(OnConnectionReadyListener listener);
+    byte[] GetOutputStd() throws IOException;
+    byte[] GetOutputErr() throws IOException;
+    SSHConnection connect();
+    SSHConnection connect(InputStream input);
+    void disconnect();
+}

+ 7 - 0
src/main/java/info/knacki/gitdroid/io/ssh/SSHFactory.java

@@ -0,0 +1,7 @@
+package info.knacki.gitdroid.io.ssh;
+
+public class SSHFactory {
+    public static SSHConnection createInstance(SSHConnection.ConnectionParams sshParams, String command) {
+        return new JSchWrapper(sshParams, command);
+    }
+}

+ 3 - 0
src/main/res/values/strings.xml

@@ -0,0 +1,3 @@
+<resources>
+    <string name="app_name">gitdroid</string>
+</resources>

+ 17 - 0
src/test/java/info/knacki/gitdroid/ExampleUnitTest.java

@@ -0,0 +1,17 @@
+package info.knacki.gitdroid;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+    @Test
+    public void addition_isCorrect() {
+        assertEquals(4, 2 + 2);
+    }
+}