diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 482091ca..5563fe1d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,6 +5,15 @@ on: pull_request: branches: [ master ] jobs: + detectonly: + name: Detect use of .only + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Detect use of .only + run: | + grep -rq --include '*.spec.js' \.only\( . && echo 'You have .only() in your tests!' && exit 1 + exit 0 default: name: "Default" runs-on: ubuntu-latest diff --git a/emscriptenbuild/post-async.js b/emscriptenbuild/post-async.js index c7d9c28c..87a23bdb 100644 --- a/emscriptenbuild/post-async.js +++ b/emscriptenbuild/post-async.js @@ -25,12 +25,11 @@ FS.chmod = function(path, mode, dontFollow) { }; if(ENVIRONMENT_IS_WEB) { - Module.oldCallMain = Module.callMain + Module.origCallMain = Module.callMain; Module.callMain = async (args) => { - await Module.oldCallMain(args); - var runningAsync = typeof Asyncify === 'object' && Asyncify.currData; - if (runningAsync) { - await new Promise((resolve) => { Asyncify.asyncFinalizers.push(() => { resolve();}); }); + await Module.origCallMain(args); + if (typeof Asyncify === 'object' && Asyncify.currData) { + await Asyncify.whenDone(); } }; diff --git a/karma.conf-async.js b/karma.conf-async.js index 292f2fd9..68375ef8 100644 --- a/karma.conf-async.js +++ b/karma.conf-async.js @@ -26,7 +26,7 @@ module.exports = function(config) { ], proxies: { - '/testrepo.git': 'http://localhost:5000/testrepo.git', + '/testrepo.git': 'http://localhost:8080/testrepo.git', }, // preprocess matching files before serving them to the browser diff --git a/karma.conf.js b/karma.conf.js index 8a85a884..17926a3d 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -27,7 +27,7 @@ module.exports = function(config) { ], proxies: { - '/testrepo.git': 'http://localhost:5000/testrepo.git', + '/testrepo.git': 'http://localhost:8080/testrepo.git', }, // preprocess matching files before serving them to the browser @@ -44,7 +44,7 @@ module.exports = function(config) { // web server port port: 9876, - + browserNoActivityTimeout: 10000, // enable / disable colors in the output (reporters and logs) colors: true, diff --git a/libgit2patchedfiles/examples/checkout.c b/libgit2patchedfiles/examples/checkout.c index 8a3dc478..db4ad1c6 100644 --- a/libgit2patchedfiles/examples/checkout.c +++ b/libgit2patchedfiles/examples/checkout.c @@ -29,6 +29,7 @@ * The following example demonstrates how to do checkouts with libgit2. * * Recognized options are : + * -b: create new branch * --force: force the checkout to happen. * --[no-]progress: show checkout progress, on by default. * --perf: show performance data. @@ -43,11 +44,12 @@ typedef struct { static void print_usage(void) { fprintf(stderr, "usage: checkout [options] \n" - "Options are :\n" - " --git-dir: use the following git repository.\n" - " --force: force the checkout.\n" - " --[no-]progress: show checkout progress.\n" - " --perf: show performance data.\n"); + "Options are :\n" + " -b: create new branch" + " --git-dir: use the following git repository.\n" + " --force: force the checkout.\n" + " --[no-]progress: show checkout progress.\n" + " --perf: show performance data.\n"); exit(1); } @@ -104,7 +106,7 @@ static void print_perf_data(const git_checkout_perfdata *perfdata, void *payload { (void)payload; printf("perf: stat: %" PRIuZ " mkdir: %" PRIuZ " chmod: %" PRIuZ "\n", - perfdata->stat_calls, perfdata->mkdir_calls, perfdata->chmod_calls); + perfdata->stat_calls, perfdata->mkdir_calls, perfdata->chmod_calls); } /** @@ -223,7 +225,7 @@ static int guess_refish(git_annotated_commit **out, git_repository *repo, const goto next; break; -next: + next: free(refname); if (error < 0 && error != GIT_ENOTFOUND) break; @@ -239,7 +241,7 @@ static int guess_refish(git_annotated_commit **out, git_repository *repo, const out: git_reference_free(remote_ref); - git_strarray_free(&remotes); + git_strarray_dispose(&remotes); return error; } @@ -250,6 +252,13 @@ int lg2_checkout(git_repository *repo, int argc, char **argv) checkout_options opts; git_repository_state_t state; git_annotated_commit *checkout_target = NULL; + git_reference *new_branch_ref = NULL; + git_object *target_obj = NULL; + git_commit *target_commit = NULL; + git_reference *branch_ref = NULL; + git_reference *upstream_ref = NULL; + + const char *opt_new_branch; int err = 0; const char *path = "."; @@ -264,6 +273,24 @@ int lg2_checkout(git_repository *repo, int argc, char **argv) goto cleanup; } + if (optional_str_arg(&opt_new_branch, &args, "-b", "")) { + err = git_revparse_single(&target_obj, repo, "HEAD"); + if (err != 0) { + fprintf(stderr, "error: %s\n", git_error_last()->message); + goto cleanup; + } + err = git_commit_lookup(&target_commit, repo, git_object_id(target_obj)); + if (err != 0) { + fprintf(stderr, "error looking up commit: %s\n", git_error_last()->message); + goto cleanup; + } + err = git_branch_create(&new_branch_ref, repo, opt_new_branch, target_commit, 0); + if (err != 0) { + fprintf(stderr, "error creating branch: %s\n", git_error_last()->message); + goto cleanup; + } + } + if (match_arg_separator(&args)) { /** * Try to checkout the given path(s) @@ -275,7 +302,7 @@ int lg2_checkout(git_repository *repo, int argc, char **argv) copts.checkout_strategy = GIT_CHECKOUT_FORCE; paths.count = args.argc - args.pos; - + if (paths.count == 0) { fprintf(stderr, "error: no paths specified\n"); return GIT_ERROR_INVALID; @@ -293,16 +320,46 @@ int lg2_checkout(git_repository *repo, int argc, char **argv) /** * Try to resolve a "refish" argument to a target libgit2 can use */ + + const char *branchname; + char upstreamname[1024] = "origin/"; + if ((err = resolve_refish(&checkout_target, repo, args.argv[args.pos])) < 0 && - (err = guess_refish(&checkout_target, repo, args.argv[args.pos])) < 0) { + (err = guess_refish(&checkout_target, repo, args.argv[args.pos])) < 0) { fprintf(stderr, "failed to resolve %s: %s\n", args.argv[args.pos], git_error_last()->message); goto cleanup; } err = perform_checkout_ref(repo, checkout_target, args.argv[args.pos], &opts); + if (err != 0) { + fprintf(stderr, "failed to checkout %s: %s\n", args.argv[args.pos], git_error_last()->message); + goto cleanup; + } + err = git_repository_head(&branch_ref, repo); + if (!err && git_branch_upstream(&upstream_ref, branch_ref) == GIT_ENOTFOUND) { + branchname = git_reference_shorthand(branch_ref); + strcat(upstreamname, branchname); + + err = git_branch_set_upstream(branch_ref,upstreamname); + if (err == GIT_ENOTFOUND) { + // no upstream exists + git_error_clear(); + err = 0; + } + if (err != 0) { + fprintf(stderr, "error: %s\n", git_error_last()->message); + goto cleanup; + } + printf("Branch '%s' set up to track remote branch '%s'\n", branchname, upstreamname); + } } cleanup: - git_annotated_commit_free(checkout_target); + git_commit_free(target_commit); + git_object_free(target_obj); + git_annotated_commit_free(checkout_target); + git_reference_free(new_branch_ref); + git_reference_free(branch_ref); + git_reference_free(upstream_ref); return err; } diff --git a/libgit2patchedfiles/examples/commit.c b/libgit2patchedfiles/examples/commit.c index cd9782de..b477e756 100644 --- a/libgit2patchedfiles/examples/commit.c +++ b/libgit2patchedfiles/examples/commit.c @@ -65,17 +65,34 @@ int lg2_commit(git_repository *repo, int argc, char **argv) check_lg2(git_signature_default(&signature, repo), "Error creating signature", NULL); - check_lg2(git_commit_create_v( - &commit_oid, - repo, - "HEAD", - signature, - signature, - NULL, - comment, - tree, - parent ? 1 : 0, parent), "Error creating commit", NULL); - + if (git_repository_state(repo) == GIT_REPOSITORY_STATE_MERGE) { + git_object * mergehead = NULL; + git_reference * mergeheadref = NULL; + + check_lg2(git_revparse_ext(&mergehead, &mergeheadref, repo, "MERGE_HEAD"), "Error looking up MERGE_HEAD", NULL); + check_lg2(git_commit_create_v( + &commit_oid, + repo, + "HEAD", + signature, + signature, + NULL, + comment, + tree, + 2, parent, mergehead), "Error creating commit", NULL); + git_repository_state_cleanup(repo); + } else { + check_lg2(git_commit_create_v( + &commit_oid, + repo, + "HEAD", + signature, + signature, + NULL, + comment, + tree, + parent ? 1 : 0, parent), "Error creating commit", NULL); + } git_index_free(index); git_signature_free(signature); git_tree_free(tree); diff --git a/libgit2patchedfiles/examples/common.h b/libgit2patchedfiles/examples/common.h index 0126568a..ec056fe7 100644 --- a/libgit2patchedfiles/examples/common.h +++ b/libgit2patchedfiles/examples/common.h @@ -74,6 +74,7 @@ extern int lg2_ls_remote(git_repository *repo, int argc, char **argv); extern int lg2_merge(git_repository *repo, int argc, char **argv); extern int lg2_push(git_repository *repo, int argc, char **argv); extern int lg2_remote(git_repository *repo, int argc, char **argv); +extern int lg2_reset(git_repository *repo, int argc, char **argv); extern int lg2_rev_list(git_repository *repo, int argc, char **argv); extern int lg2_rev_parse(git_repository *repo, int argc, char **argv); extern int lg2_show_index(git_repository *repo, int argc, char **argv); diff --git a/libgit2patchedfiles/examples/lg2.c b/libgit2patchedfiles/examples/lg2.c index 7946bc21..3a4f1c60 100644 --- a/libgit2patchedfiles/examples/lg2.c +++ b/libgit2patchedfiles/examples/lg2.c @@ -30,6 +30,7 @@ struct { { "merge", lg2_merge, 1 }, { "push", lg2_push, 1 }, { "remote", lg2_remote, 1 }, + { "reset", lg2_reset, 1 }, { "rev-list", lg2_rev_list, 1 }, { "rev-parse", lg2_rev_parse, 1 }, { "show-index", lg2_show_index, 0 }, diff --git a/libgit2patchedfiles/examples/push.c b/libgit2patchedfiles/examples/push.c index bcf30760..3072eb0c 100644 --- a/libgit2patchedfiles/examples/push.c +++ b/libgit2patchedfiles/examples/push.c @@ -33,7 +33,12 @@ int lg2_push(git_repository *repo, int argc, char **argv) { git_push_options options; git_remote* remote = NULL; - char *refspec = "refs/heads/master"; + char *refspec = NULL; + git_reference* head_ref; + + git_reference_lookup(&head_ref, repo, "HEAD"); + refspec = git_reference_symbolic_target(head_ref); + const git_strarray refspecs = { &refspec, 1 @@ -52,5 +57,6 @@ int lg2_push(git_repository *repo, int argc, char **argv) { check_lg2(git_remote_push(remote, &refspecs, &options), "Error pushing", NULL); printf("pushed\n"); + git_reference_free(head_ref); return 0; } diff --git a/libgit2patchedfiles/examples/reset.c b/libgit2patchedfiles/examples/reset.c new file mode 100644 index 00000000..7021e137 --- /dev/null +++ b/libgit2patchedfiles/examples/reset.c @@ -0,0 +1,69 @@ +/* + * libgit2 "reset" example - Reset current HEAD to the specified state + * + * Written by the libgit2 contributors + * + * To the extent possible under law, the author(s) have dedicated all copyright + * and related and neighboring rights to this software to the public domain + * worldwide. This software is distributed without any warranty. + * + * You should have received a copy of the CC0 Public Domain Dedication along + * with this software. If not, see + * . + */ + +#include "common.h" + +/** + * The following example demonstrates how to reset libgit2. + * + * It will use the repository in the current working directory, and reset to the specified revspec + * + * Recognized options are: + * --hard: reset hard + * --soft: reset soft + */ + +int lg2_reset(git_repository *repo, int argc, char **argv) +{ + git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; + git_commit *target_commit = NULL; + git_revspec revspec; + git_reset_t reset_type = GIT_RESET_MIXED; + int err; + + err = git_revparse(&revspec, repo, argv[argc - 1]); + if (err != 0) + { + fprintf(stderr, "failed to lookup rev: %s\n", git_error_last()->message); + goto cleanup; + } + err = git_commit_lookup(&target_commit, repo, revspec.from); + if (err != 0) + { + fprintf(stderr, "failed to lookup commit: %s\n", git_error_last()->message); + goto cleanup; + } + + if (argc > 1) + { + if (!strcmp(argv[argc - 2], "--hard")) + { + reset_type = GIT_RESET_HARD; + } + else if (!strcmp(argv[argc - 2], "--soft")) + { + reset_type = GIT_RESET_SOFT; + } + } + err = git_reset(repo, target_commit, reset_type, &checkout_opts); + if (err != 0) + { + fprintf(stderr, "reset error: %s\n", git_error_last()->message); + goto cleanup; + } +cleanup: + git_commit_free(target_commit); + + return 0; +} diff --git a/libgit2patchedfiles/examples/status.c b/libgit2patchedfiles/examples/status.c new file mode 100644 index 00000000..4b117b23 --- /dev/null +++ b/libgit2patchedfiles/examples/status.c @@ -0,0 +1,603 @@ +/* + * libgit2 "status" example - shows how to use the status APIs + * + * Written by the libgit2 contributors + * + * To the extent possible under law, the author(s) have dedicated all copyright + * and related and neighboring rights to this software to the public domain + * worldwide. This software is distributed without any warranty. + * + * You should have received a copy of the CC0 Public Domain Dedication along + * with this software. If not, see + * . + */ + +#include "common.h" + +/** + * This example demonstrates the use of the libgit2 status APIs, + * particularly the `git_status_list` object, to roughly simulate the + * output of running `git status`. It serves as a simple example of + * using those APIs to get basic status information. + * + * This does not have: + * + * - Robust error handling + * - Colorized or paginated output formatting + * + * This does have: + * + * - Examples of translating command line arguments to the status + * options settings to mimic `git status` results. + * - A sample status formatter that matches the default "long" format + * from `git status` + * - A sample status formatter that matches the "short" format + * - Count of commits ahead/behind the remote upstream + */ + +enum +{ + FORMAT_DEFAULT = 0, + FORMAT_LONG = 1, + FORMAT_SHORT = 2, + FORMAT_PORCELAIN = 3, +}; + +#define MAX_PATHSPEC 8 + +struct status_opts +{ + git_status_options statusopt; + char *repodir; + char *pathspec[MAX_PATHSPEC]; + int npaths; + int format; + int zterm; + int showbranch; + int showsubmod; + int repeat; +}; + +static void parse_opts(struct status_opts *o, int argc, char *argv[]); +static void show_branch(git_repository *repo, int format); +static void show_ahead_behind(git_repository *repo); +static void print_conflicts(git_repository *repo); +static void print_long(git_status_list *status); +static void print_short(git_repository *repo, git_status_list *status); +static int print_submod(git_submodule *sm, const char *name, void *payload); + +int lg2_status(git_repository *repo, int argc, char *argv[]) +{ + git_status_list *status; + struct status_opts o = {GIT_STATUS_OPTIONS_INIT, "."}; + + o.statusopt.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; + o.statusopt.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX | + GIT_STATUS_OPT_SORT_CASE_SENSITIVELY; + + parse_opts(&o, argc, argv); + + if (git_repository_is_bare(repo)) + fatal("Cannot report status on bare repository", + git_repository_path(repo)); + +show_status: + if (o.repeat) + printf("\033[H\033[2J"); + + /** + * Run status on the repository + * + * We use `git_status_list_new()` to generate a list of status + * information which lets us iterate over it at our + * convenience and extract the data we want to show out of + * each entry. + * + * You can use `git_status_foreach()` or + * `git_status_foreach_ext()` if you'd prefer to execute a + * callback for each entry. The latter gives you more control + * about what results are presented. + */ + check_lg2(git_status_list_new(&status, repo, &o.statusopt), + "Could not get status", NULL); + + if (o.showbranch) + show_branch(repo, o.format); + + show_ahead_behind(repo); + + if (o.showsubmod) + { + int submod_count = 0; + check_lg2(git_submodule_foreach(repo, print_submod, &submod_count), + "Cannot iterate submodules", o.repodir); + } + + if (o.format == FORMAT_LONG) + print_long(status); + else + print_short(repo, status); + + print_conflicts(repo); + + git_status_list_free(status); + + if (o.repeat) + { + sleep(o.repeat); + goto show_status; + } + + return 0; +} + +/** + * If the user asked for the branch, let's show the short name of the + * branch. + */ +static void show_branch(git_repository *repo, int format) +{ + int error = 0; + const char *branch = NULL; + git_reference *head = NULL; + + error = git_repository_head(&head, repo); + + if (error == GIT_EUNBORNBRANCH || error == GIT_ENOTFOUND) + branch = NULL; + else if (!error) + { + branch = git_reference_shorthand(head); + } + else + check_lg2(error, "failed to get current branch", NULL); + + if (format == FORMAT_LONG) + printf("# On branch %s\n", + branch ? branch : "Not currently on any branch."); + else + printf("## %s\n", branch ? branch : "HEAD (no branch)"); + + git_reference_free(head); +} + +static void show_ahead_behind(git_repository *repo) +{ + git_reference *upstream = NULL; + git_reference *head_ref = NULL; + size_t ahead; + size_t behind; + int err; + + err = git_repository_head(&head_ref, repo); + if (err != 0) + { + fprintf(stderr, "error: %s\n", git_error_last()->message); + goto cleanup; + } + + err = git_branch_upstream(&upstream, head_ref); + if (err != 0) + { + goto cleanup; + } + err = git_graph_ahead_behind(&ahead, &behind, repo, git_reference_target(head_ref), git_reference_target(upstream)); + if (err != 0) + { + fprintf(stderr, "error: %s\n", git_error_last()->message); + goto cleanup; + } + if (ahead > 0 || behind > 0) { + printf("# Your branch is ahead by %d, behind by %d commits.\n", ahead, behind); + } +cleanup: + git_reference_free(head_ref); + git_reference_free(upstream); +} + +/** + * Simple printing of conflicts + */ +static void print_conflicts(git_repository *repo) { + git_index *index = NULL; + git_repository_index(&index, repo); + + if(git_index_has_conflicts(index)) { + git_index_conflict_iterator *conflicts; + const git_index_entry *ancestor = NULL; + const git_index_entry *our = NULL; + const git_index_entry *their = NULL; + int err = 0; + + git_index_conflict_iterator_new(&conflicts, index); + + while ((err = git_index_conflict_next(&ancestor, &our, &their, conflicts)) == 0) { + printf("conflict: a:%s o:%s t:%s\n", + ancestor!=NULL ? ancestor->path : "NULL", + our!=NULL ? our->path : "NULL", + their!=NULL ? their->path : "NULL"); + } + + if (err != GIT_ITEROVER) { + fprintf(stderr, "error iterating conflicts\n"); + } + + git_index_conflict_iterator_free(conflicts); + } + git_index_free(index); +} + +/** + * This function print out an output similar to git's status command + * in long form, including the command-line hints. + */ +static void print_long(git_status_list *status) +{ + size_t i, maxi = git_status_list_entrycount(status); + const git_status_entry *s; + int header = 0, changes_in_index = 0; + int changed_in_workdir = 0, rm_in_workdir = 0; + const char *old_path, *new_path; + + /** Print index changes. */ + + for (i = 0; i < maxi; ++i) + { + char *istatus = NULL; + + s = git_status_byindex(status, i); + + if (s->status == GIT_STATUS_CURRENT) + continue; + + if (s->status & GIT_STATUS_WT_DELETED) + rm_in_workdir = 1; + + if (s->status & GIT_STATUS_INDEX_NEW) + istatus = "new file: "; + if (s->status & GIT_STATUS_INDEX_MODIFIED) + istatus = "modified: "; + if (s->status & GIT_STATUS_INDEX_DELETED) + istatus = "deleted: "; + if (s->status & GIT_STATUS_INDEX_RENAMED) + istatus = "renamed: "; + if (s->status & GIT_STATUS_INDEX_TYPECHANGE) + istatus = "typechange:"; + + if (istatus == NULL) + continue; + + if (!header) + { + printf("# Changes to be committed:\n"); + printf("# (use \"git reset HEAD ...\" to unstage)\n"); + printf("#\n"); + header = 1; + } + + old_path = s->head_to_index->old_file.path; + new_path = s->head_to_index->new_file.path; + + if (old_path && new_path && strcmp(old_path, new_path)) + printf("#\t%s %s -> %s\n", istatus, old_path, new_path); + else + printf("#\t%s %s\n", istatus, old_path ? old_path : new_path); + } + + if (header) + { + changes_in_index = 1; + printf("#\n"); + } + header = 0; + + /** Print workdir changes to tracked files. */ + + for (i = 0; i < maxi; ++i) + { + char *wstatus = NULL; + + s = git_status_byindex(status, i); + + /** + * With `GIT_STATUS_OPT_INCLUDE_UNMODIFIED` (not used in this example) + * `index_to_workdir` may not be `NULL` even if there are + * no differences, in which case it will be a `GIT_DELTA_UNMODIFIED`. + */ + if (s->status == GIT_STATUS_CURRENT || s->index_to_workdir == NULL) + continue; + + /** Print out the output since we know the file has some changes */ + if (s->status & GIT_STATUS_WT_MODIFIED) + wstatus = "modified: "; + if (s->status & GIT_STATUS_WT_DELETED) + wstatus = "deleted: "; + if (s->status & GIT_STATUS_WT_RENAMED) + wstatus = "renamed: "; + if (s->status & GIT_STATUS_WT_TYPECHANGE) + wstatus = "typechange:"; + + if (wstatus == NULL) + continue; + + if (!header) + { + printf("# Changes not staged for commit:\n"); + printf("# (use \"git add%s ...\" to update what will be committed)\n", rm_in_workdir ? "/rm" : ""); + printf("# (use \"git checkout -- ...\" to discard changes in working directory)\n"); + printf("#\n"); + header = 1; + } + + old_path = s->index_to_workdir->old_file.path; + new_path = s->index_to_workdir->new_file.path; + + if (old_path && new_path && strcmp(old_path, new_path)) + printf("#\t%s %s -> %s\n", wstatus, old_path, new_path); + else + printf("#\t%s %s\n", wstatus, old_path ? old_path : new_path); + } + + if (header) + { + changed_in_workdir = 1; + printf("#\n"); + } + + /** Print untracked files. */ + + header = 0; + + for (i = 0; i < maxi; ++i) + { + s = git_status_byindex(status, i); + + if (s->status == GIT_STATUS_WT_NEW) + { + + if (!header) + { + printf("# Untracked files:\n"); + printf("# (use \"git add ...\" to include in what will be committed)\n"); + printf("#\n"); + header = 1; + } + + printf("#\t%s\n", s->index_to_workdir->old_file.path); + } + } + + header = 0; + + /** Print ignored files. */ + + for (i = 0; i < maxi; ++i) + { + s = git_status_byindex(status, i); + + if (s->status == GIT_STATUS_IGNORED) + { + + if (!header) + { + printf("# Ignored files:\n"); + printf("# (use \"git add -f ...\" to include in what will be committed)\n"); + printf("#\n"); + header = 1; + } + + printf("#\t%s\n", s->index_to_workdir->old_file.path); + } + } + + if (!changes_in_index && changed_in_workdir) + printf("no changes added to commit (use \"git add\" and/or \"git commit -a\")\n"); +} + +/** + * This version of the output prefixes each path with two status + * columns and shows submodule status information. + */ +static void print_short(git_repository *repo, git_status_list *status) +{ + size_t i, maxi = git_status_list_entrycount(status); + const git_status_entry *s; + char istatus, wstatus; + const char *extra, *a, *b, *c; + + for (i = 0; i < maxi; ++i) + { + s = git_status_byindex(status, i); + + if (s->status == GIT_STATUS_CURRENT) + continue; + + a = b = c = NULL; + istatus = wstatus = ' '; + extra = ""; + + if (s->status & GIT_STATUS_INDEX_NEW) + istatus = 'A'; + if (s->status & GIT_STATUS_INDEX_MODIFIED) + istatus = 'M'; + if (s->status & GIT_STATUS_INDEX_DELETED) + istatus = 'D'; + if (s->status & GIT_STATUS_INDEX_RENAMED) + istatus = 'R'; + if (s->status & GIT_STATUS_INDEX_TYPECHANGE) + istatus = 'T'; + + if (s->status & GIT_STATUS_WT_NEW) + { + if (istatus == ' ') + istatus = '?'; + wstatus = '?'; + } + if (s->status & GIT_STATUS_WT_MODIFIED) + wstatus = 'M'; + if (s->status & GIT_STATUS_WT_DELETED) + wstatus = 'D'; + if (s->status & GIT_STATUS_WT_RENAMED) + wstatus = 'R'; + if (s->status & GIT_STATUS_WT_TYPECHANGE) + wstatus = 'T'; + + if (s->status & GIT_STATUS_IGNORED) + { + istatus = '!'; + wstatus = '!'; + } + + if (istatus == '?' && wstatus == '?') + continue; + + /** + * A commit in a tree is how submodules are stored, so + * let's go take a look at its status. + */ + if (s->index_to_workdir && + s->index_to_workdir->new_file.mode == GIT_FILEMODE_COMMIT) + { + unsigned int smstatus = 0; + + if (!git_submodule_status(&smstatus, repo, s->index_to_workdir->new_file.path, + GIT_SUBMODULE_IGNORE_UNSPECIFIED)) + { + if (smstatus & GIT_SUBMODULE_STATUS_WD_MODIFIED) + extra = " (new commits)"; + else if (smstatus & GIT_SUBMODULE_STATUS_WD_INDEX_MODIFIED) + extra = " (modified content)"; + else if (smstatus & GIT_SUBMODULE_STATUS_WD_WD_MODIFIED) + extra = " (modified content)"; + else if (smstatus & GIT_SUBMODULE_STATUS_WD_UNTRACKED) + extra = " (untracked content)"; + } + } + + /** + * Now that we have all the information, format the output. + */ + + if (s->head_to_index) + { + a = s->head_to_index->old_file.path; + b = s->head_to_index->new_file.path; + } + if (s->index_to_workdir) + { + if (!a) + a = s->index_to_workdir->old_file.path; + if (!b) + b = s->index_to_workdir->old_file.path; + c = s->index_to_workdir->new_file.path; + } + + if (istatus == 'R') + { + if (wstatus == 'R') + printf("%c%c %s %s %s%s\n", istatus, wstatus, a, b, c, extra); + else + printf("%c%c %s %s%s\n", istatus, wstatus, a, b, extra); + } + else + { + if (wstatus == 'R') + printf("%c%c %s %s%s\n", istatus, wstatus, a, c, extra); + else + printf("%c%c %s%s\n", istatus, wstatus, a, extra); + } + } + + for (i = 0; i < maxi; ++i) + { + s = git_status_byindex(status, i); + + if (s->status == GIT_STATUS_WT_NEW) + printf("?? %s\n", s->index_to_workdir->old_file.path); + } +} + +static int print_submod(git_submodule *sm, const char *name, void *payload) +{ + int *count = payload; + (void)name; + + if (*count == 0) + printf("# Submodules\n"); + (*count)++; + + printf("# - submodule '%s' at %s\n", + git_submodule_name(sm), git_submodule_path(sm)); + + return 0; +} + +/** + * Parse options that git's status command supports. + */ +static void parse_opts(struct status_opts *o, int argc, char *argv[]) +{ + struct args_info args = ARGS_INFO_INIT; + + for (args.pos = 1; args.pos < argc; ++args.pos) + { + char *a = argv[args.pos]; + + if (a[0] != '-') + { + if (o->npaths < MAX_PATHSPEC) + o->pathspec[o->npaths++] = a; + else + fatal("Example only supports a limited pathspec", NULL); + } + else if (!strcmp(a, "-s") || !strcmp(a, "--short")) + o->format = FORMAT_SHORT; + else if (!strcmp(a, "--long")) + o->format = FORMAT_LONG; + else if (!strcmp(a, "--porcelain")) + o->format = FORMAT_PORCELAIN; + else if (!strcmp(a, "-b") || !strcmp(a, "--branch")) + o->showbranch = 1; + else if (!strcmp(a, "-z")) + { + o->zterm = 1; + if (o->format == FORMAT_DEFAULT) + o->format = FORMAT_PORCELAIN; + } + else if (!strcmp(a, "--ignored")) + o->statusopt.flags |= GIT_STATUS_OPT_INCLUDE_IGNORED; + else if (!strcmp(a, "-uno") || + !strcmp(a, "--untracked-files=no")) + o->statusopt.flags &= ~GIT_STATUS_OPT_INCLUDE_UNTRACKED; + else if (!strcmp(a, "-unormal") || + !strcmp(a, "--untracked-files=normal")) + o->statusopt.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED; + else if (!strcmp(a, "-uall") || + !strcmp(a, "--untracked-files=all")) + o->statusopt.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS; + else if (!strcmp(a, "--ignore-submodules=all")) + o->statusopt.flags |= GIT_STATUS_OPT_EXCLUDE_SUBMODULES; + else if (!strncmp(a, "--git-dir=", strlen("--git-dir="))) + o->repodir = a + strlen("--git-dir="); + else if (!strcmp(a, "--repeat")) + o->repeat = 10; + else if (match_int_arg(&o->repeat, &args, "--repeat", 0)) + /* okay */; + else if (!strcmp(a, "--list-submodules")) + o->showsubmod = 1; + else + check_lg2(-1, "Unsupported option", a); + } + + if (o->format == FORMAT_DEFAULT) + o->format = FORMAT_LONG; + if (o->format == FORMAT_LONG) + o->showbranch = 1; + if (o->npaths > 0) + { + o->statusopt.pathspec.strings = o->pathspec; + o->statusopt.pathspec.count = o->npaths; + } +} diff --git a/nodefsclonetest b/nodefsclonetest new file mode 160000 index 00000000..5119c56b --- /dev/null +++ b/nodefsclonetest @@ -0,0 +1 @@ +Subproject commit 5119c56b5ca51423548c2f0729b180b2681590ef diff --git a/package-lock.json b/package-lock.json index 1eef4bf2..f92020c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "wasm-git", - "version": "0.0.6", + "version": "0.0.7", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 65e845f0..ac33ce0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wasm-git", - "version": "0.0.6", + "version": "0.0.7", "author": { "name": "Peter Salomonsen", "url": "https://petersalomonsen.com" @@ -16,7 +16,9 @@ "scripts": { "test": "mocha test/**/*.spec.js", "test-browser": "karma start --single-run --browsers ChromeHeadless karma.conf.js", - "test-browser-async": "karma start --single-run --browsers ChromeHeadless karma.conf-async.js" + "test-browser-watch": "karma start --browsers Chrome karma.conf.js", + "test-browser-async": "karma start --single-run --browsers ChromeHeadless karma.conf-async.js", + "patch-version": "npm --no-git-tag-version version patch" }, "devDependencies": { "cgi": "^0.3.1", diff --git a/setup.sh b/setup.sh index 734caedd..f7219f20 100755 --- a/setup.sh +++ b/setup.sh @@ -1,6 +1,6 @@ -curl -L https://github.com/libgit2/libgit2/releases/download/v1.0.1/libgit2-1.0.1.tar.gz --output libgit2.tar.gz +curl -L https://github.com/libgit2/libgit2/archive/refs/tags/v1.3.0.tar.gz --output libgit2.tar.gz tar -xzf libgit2.tar.gz -mv libgit2-1.0.1 libgit2 +mv libgit2-1.3.0 libgit2 rm libgit2.tar.gz rm libgit2/src/transports/http.c cp -r libgit2patchedfiles/* libgit2/ diff --git a/test-browser-async/test.spec.js b/test-browser-async/test.spec.js index aa2818c5..5bf9b72c 100644 --- a/test-browser-async/test.spec.js +++ b/test-browser-async/test.spec.js @@ -1,4 +1,6 @@ describe('wasm-git', function() { + this.timeout(20000); + it('should have window', () => { assert(window !== undefined); }); diff --git a/test-browser/githttpserver.js b/test-browser/githttpserver.js index 37e748e2..b6facb4b 100644 --- a/test-browser/githttpserver.js +++ b/test-browser/githttpserver.js @@ -5,11 +5,13 @@ function startServer() { const http = require('http'); + const fs = require('fs'); const cgi = require('cgi'); const { tmpdir } = require('os'); const { execSync } = require('child_process'); + fs.rmdirSync(`${tmpdir()}/testrepo.git`, {recursive: true, force: true}); execSync(`git init --bare ${tmpdir()}/testrepo.git`); const script = 'git'; @@ -38,7 +40,7 @@ function startServer() { response.statusCode = 404; response.end('not found'); } - }).listen(5000); + }).listen(8080); } module.exports = { diff --git a/test-browser/stashpop.spec.js b/test-browser/stashpop.spec.js new file mode 100644 index 00000000..3b0247c5 --- /dev/null +++ b/test-browser/stashpop.spec.js @@ -0,0 +1,123 @@ +/* + Expected output with regular git (the difference is in the last status): + +peter@MacBook-Air wgtest % echo "hei" > test.txt +peter@MacBook-Air wgtest % git add test.txt +peter@MacBook-Air wgtest % git commit -m "hei" +[master (root-commit) 6059cdd] hei + 1 file changed, 1 insertion(+) + create mode 100644 test.txt +peter@MacBook-Air wgtest % git checkout -b testbranch +Switched to a new branch 'testbranch' +peter@MacBook-Air wgtest % echo "hei" > test2.txt +peter@MacBook-Air wgtest % git add test2.txt +peter@MacBook-Air wgtest % git commit -m "heia" +[testbranch 4892397] heia + 1 file changed, 1 insertion(+) + create mode 100644 test2.txt +peter@MacBook-Air wgtest % echo "hei2" > test2.txt +peter@MacBook-Air wgtest % git checkout master +error: Your local changes to the following files would be overwritten by checkout: + test2.txt +Please commit your changes or stash them before you switch branches. +Aborting +peter@MacBook-Air wgtest % git stash +Saved working directory and index state WIP on testbranch: 4892397 heia +peter@MacBook-Air wgtest % git checkout master +Switched to branch 'master' +peter@MacBook-Air wgtest % git stash pop +CONFLICT (modify/delete): test2.txt deleted in Updated upstream and modified in Stashed changes. Version Stashed changes of test2.txt left in tree. +The stash entry is kept in case you need it again. +peter@MacBook-Air wgtest % git status --short +DU test2.txt +peter@MacBook-Air wgtest % git status +On branch master +Unmerged paths: + (use "git restore --staged ..." to unstage) + (use "git add/rm ..." as appropriate to mark resolution) + deleted by us: test2.txt + +no changes added to commit (use "git add" and/or "git commit -a") +peter@MacBook-Air wgtest % git status --short +DU test2.txt + + */ + +describe('wasm-git-stash-pop', function () { + this.timeout(20000); + + let worker; + + const createWorker = async () => { + worker = new Worker('base/worker.js'); + await new Promise(resolve => { + worker.onmessage = msg => { + if (msg.data.ready) { + resolve(msg); + } + } + }); + } + + const callWorker = async (command, params) => { + return await new Promise(resolve => { + worker.onmessage = msg => resolve(msg.data); + worker.postMessage(Object.assign({ + command: command + }, params)); + }); + }; + const callWorkerWithArgs = async (command, ...args) => { + return await new Promise(resolve => { + worker.onmessage = msg => resolve(msg.data) + worker.postMessage({ + command: command, + args: args + }); + }); + }; + + this.beforeAll(async () => { + await createWorker(); + await callWorker('synclocal', {url: `${location.origin}/teststash.git`, newrepo: true }); + }); + + this.afterAll(async () => { + assert.equal((await callWorker('deletelocal')).deleted, 'teststash.git'); + worker.terminate(); + }); + + it('should create repo pop and stash, and show conflict', async () => { + await callWorkerWithArgs('init', '.'); + await callWorker( + 'writefile', { + filename: 'test.txt', + contents: 'blabla' + }); + await callWorkerWithArgs('add', 'test.txt'); + await callWorkerWithArgs('commit', '-m', 'first commit'); + await callWorkerWithArgs('checkout', '-b', 'testbranch'); + await callWorker( + 'writefile', { + filename: 'test2.txt', + contents: 'blabla' + }); + await callWorkerWithArgs('add', 'test2.txt'); + await callWorkerWithArgs('commit', '-m', 'second commit'); + await callWorker( + 'writefile', { + filename: 'test2.txt', + contents: 'blabla2' + }); + assert.isTrue((await callWorkerWithArgs('checkout', 'master')).stderr.indexOf('1 conflict prevents checkout')>-1); + await callWorkerWithArgs('stash'); + assert.equal((await callWorkerWithArgs('checkout', 'master')).stderr,''); + assert.equal((await callWorker('dir')).dircontents.findIndex(f => f==='test2.txt'), -1); + await callWorkerWithArgs('stash', 'pop'); + assert.isTrue((await callWorker('dir')).dircontents.findIndex(f => f==='test2.txt')>-1); + // This part is different in libgit2 status from cmd line git + // assert.equal((await callWorker('status')).stdout, 'DU test2.txt'); + console.log((await callWorker('status')).stdout); + assert.isTrue((await callWorker('status')).stdout.indexOf('conflict: a:test2.txt o:NULL t:test2.txt') >0 ); + }); +}); \ No newline at end of file diff --git a/test-browser/test.spec.js b/test-browser/test.spec.js index c3a2c417..d6ca72b2 100644 --- a/test-browser/test.spec.js +++ b/test-browser/test.spec.js @@ -1,17 +1,44 @@ -describe('wasm-git', function() { - const worker = new Worker('base/worker.js'); +describe('wasm-git', function () { + this.timeout(20000); - const workerReadyPromise = new Promise(resolve => { - worker.onmessage = msg => { - if (msg.data.ready) { - resolve(msg); + let worker; + + const createWorker = async () => { + worker = new Worker('base/worker.js'); + await new Promise(resolve => { + worker.onmessage = msg => { + if (msg.data.ready) { + resolve(msg); + } } - } + }); + } + + const callWorker = async (command, params) => { + return await new Promise(resolve => { + worker.onmessage = msg => resolve(msg.data); + worker.postMessage(Object.assign({ + command: command + }, params)); + }); + }; + const callWorkerWithArgs = async (command, ...args) => { + return await new Promise(resolve => { + worker.onmessage = msg => resolve(msg.data) + worker.postMessage({ + command: command, + args: args + }); + }); + }; + + this.afterAll(async () => { + assert.equal((await callWorker('deletelocal')).deleted, 'testrepo.git'); + worker.terminate(); }); it('should get ready message from web worker', async () => { - const msg = await workerReadyPromise; - assert(msg.data.ready); + await createWorker(); }); it('should ping the gitserver', async () => { @@ -20,28 +47,28 @@ describe('wasm-git', function() { }); it('should sync local idbfs and find no repository', async () => { - worker.postMessage({command: 'synclocal', url: `${location.origin}/testrepo.git`}); + worker.postMessage({ command: 'synclocal', url: `${location.origin}/testrepo.git` }); let result = await new Promise(resolve => worker.onmessage = msg => { if (msg.data.notfound) { resolve(msg); } else { console.log(msg.data); - } + } } ); assert(result.data.notfound); }); it('should clone a bare repository and push commits', async () => { - worker.postMessage({command: 'clone', url: `${location.origin}/testrepo.git`}); + worker.postMessage({ command: 'clone', url: `${location.origin}/testrepo.git` }); let result = await new Promise(resolve => worker.onmessage = msg => { if (msg.data.dircontents) { resolve(msg); } else { console.log(msg.data); - } + } } ); assert(result.data.dircontents.length > 2); @@ -58,64 +85,127 @@ describe('wasm-git', function() { resolve(msg); } else { console.log(msg.data); - } + } } ); assert(result.data.dircontents.find(entry => entry === 'test.txt')); + console.log(`1 second pause to make sure we don't get another log entry in the same second`); + await new Promise(r => setTimeout(r, 1000)); }); - it('remove the local clone of the repository', async () => { - worker.postMessage({command: 'deletelocal'}); - let result = await new Promise(resolve => - worker.onmessage = msg => { - if (msg.data.deleted) { - resolve(msg); - } else { - console.log(msg.data); - } - } - ); - assert.equal(result.data.deleted, 'testrepo.git'); + it('remove the local clone of the repository', async () => { + assert.equal((await callWorker('deletelocal')).deleted, 'testrepo.git'); + worker.terminate(); }); - it('should clone the repository with contents', async () => { - worker.postMessage({command: 'readfile', filename: 'test.txt'}); - let result = await new Promise(resolve => - worker.onmessage = msg => { - if (msg.data.stderr) { - resolve(msg); - } else { - console.log(msg.data); - } - } - ); - assert.exists(result.data.stderr); + await createWorker(); + assert.isTrue((await callWorker('synclocal', {url: `${location.origin}/testrepo.git` })).notfound); - worker.postMessage({command: 'clone', url: `${location.origin}/testrepo.git`}); + let result = await callWorker('readfile', { filename: 'test.txt' }); + assert.exists(result.stderr); + + worker.postMessage({ command: 'clone', url: `${location.origin}/testrepo.git` }); result = await new Promise(resolve => worker.onmessage = msg => { if (msg.data.dircontents) { resolve(msg); } else { console.log(msg.data); - } + } } ); assert(result.data.dircontents.length > 2); assert(result.data.dircontents.find(entry => entry === '.git')); assert(result.data.dircontents.find(entry => entry === 'test.txt')); - worker.postMessage({command: 'readfile', filename: 'test.txt'}); + worker.postMessage({ command: 'readfile', filename: 'test.txt' }); result = await new Promise(resolve => worker.onmessage = msg => { if (msg.data.filecontents) { resolve(msg); } else { console.log(msg.data); - } + } } ); assert.equal(result.data.filecontents, 'hello world!'); + }); + it('should create new branch', async () => { + await callWorkerWithArgs('checkout', 'testbranch'); + assert.equal((await callWorker('status')).stdout.split('\n')[0], '# On branch master'); + await callWorkerWithArgs('checkout', '-b', 'testbranch'); + assert.equal((await callWorker('status')).stdout.split('\n')[0], '# On branch testbranch'); + }); + it('should reset to HEAD~1', async () => { + await callWorker( + 'writecommitandpush', { + filename: 'test2.txt', + contents: 'hello world2!' + }); + const commitLogBeforeReset = (await callWorker('log')).stdout.split('\n').filter(l => l.indexOf('commit ')===0); + await callWorkerWithArgs('reset', 'HEAD~1'); + const commitLogAfterReset = (await callWorker('log')).stdout.split('\n').filter(l => l.indexOf('commit ')===0); + assert.equal(commitLogAfterReset[0],commitLogBeforeReset[1]); + assert.isTrue((await callWorker('dir')).dircontents.indexOf('test2.txt')>-1); + }); + it('should show 1 ahead', async() => { + await callWorkerWithArgs('checkout', 'master'); + await callWorkerWithArgs('status'); + await callWorkerWithArgs('add', 'test2.txt'); + await callWorkerWithArgs('commit', '-m', 'add test2'); + const aheadbehind = (await callWorkerWithArgs('status')).stdout.split('\n')[1]; + assert.equal('# Your branch is ahead by 1, behind by 0 commits.',aheadbehind); + }); + it('should find the new branch after cloning', async() => { + assert.equal((await callWorker('deletelocal')).deleted, 'testrepo.git'); + worker.terminate(); + await createWorker(); + assert.isTrue((await callWorker('synclocal', {url: `${location.origin}/testrepo.git` })).notfound); + await callWorker('clone', {url: `${location.origin}/testrepo.git` }); + await callWorkerWithArgs('checkout', 'testbranch'); + assert.equal((await callWorker('status')).stdout.split('\n')[0], '# On branch testbranch'); + }); + it('the new branch should have been set up with remote tracking', async() => { + let config = (await callWorker('readfile', { filename: '.git/config' })).filecontents; + assert.isTrue(config.indexOf(`[branch "testbranch"]`) > -1) + }); + it('should reset hard to HEAD~1', async () => { + const commitLogBeforeCommit = (await callWorker('log')).stdout.split('\n').filter(l => l.indexOf('commit ')===0); + console.log('before commit', commitLogBeforeCommit[0], commitLogBeforeCommit[1], commitLogBeforeCommit[2]); + let result = await callWorker( + 'writecommitandpush', { + filename: 'testResetHard.txt', + contents: 'hello world! reset hard' + }); + assert.isTrue(result.dircontents.indexOf('testResetHard.txt')>0); + const commitLogBeforeReset = (await callWorker('log')).stdout.split('\n').filter(l => l.indexOf('commit ')===0); + console.log('before reset', commitLogBeforeReset[0], commitLogBeforeReset[1], commitLogBeforeReset[2]); + result = await callWorkerWithArgs('reset', '--hard', 'HEAD~1'); + console.log('stderr after reset',result.stderr); + const commitLogAfterReset = (await callWorker('log')).stdout.split('\n').filter(l => l.indexOf('commit ')===0); + console.log('after reset', commitLogAfterReset[0], commitLogAfterReset[1]); + assert.equal(commitLogAfterReset[0],commitLogBeforeReset[1]); + assert.equal((await callWorker('dir')).dircontents.indexOf('testResetHard.txt'),-1); + }); + it('should show 1 behind after previous hard reset', async() => { + const aheadbehind = (await callWorkerWithArgs('status')).stdout.split('\n')[1]; + assert.equal(aheadbehind, '# Your branch is ahead by 0, behind by 1 commits.'); + }); + it('should be able to create a new branch on a new repo that is not cloned', async() => { + assert.equal((await callWorker('deletelocal')).deleted, 'testrepo.git'); + worker.terminate(); + await createWorker(); + assert.isTrue((await callWorker('synclocal', {url: `${location.origin}/testrepo.git`, newrepo: true })).empty); + await callWorkerWithArgs('init', '.'); + await callWorker( + 'writefile', { + filename: 'test44.txt', + contents: 'hello world5!' + }); + await callWorkerWithArgs('add', 'test44.txt'); + await callWorkerWithArgs('commit', '-m', 'another test commit'); + assert.equal((await callWorkerWithArgs('checkout', '-b', 'testbranch')).stderr, ''); + assert.equal((await callWorker('status')).stdout.split('\n')[0], '# On branch testbranch'); }); -}); \ No newline at end of file +}); diff --git a/test-browser/worker.js b/test-browser/worker.js index 0aec8104..ecd14bd2 100644 --- a/test-browser/worker.js +++ b/test-browser/worker.js @@ -1,93 +1,104 @@ -var Module = { - 'print': function(text) { - - console.log(text); - postMessage({'stdout': text}); - }, - 'printErr': function(text) { - - console.error(text); - postMessage({'stderr': text}); - } +let stdout = []; +let stderr = []; + +var Module = { + 'print': function (text) { + console.log(text); + stdout.push(text) + }, + 'printErr': function (text) { + console.error(text); + stderr.push(text); + } }; importScripts('lg2.js'); Module.onRuntimeInitialized = () => { - const lg = Module; + const lg = Module; + + FS.writeFile('/home/web_user/.gitconfig', '[user]\n' + + 'name = Test User\n' + + 'email = test@example.com'); - FS.writeFile('/home/web_user/.gitconfig', '[user]\n' + - 'name = Test User\n' + - 'email = test@example.com'); + let currentRepoRootDir; + + onmessage = (msg) => { + stderr = []; + stdout = []; + if (msg.data.command === 'writecommitandpush') { + FS.writeFile(msg.data.filename, msg.data.contents); + lg.callMain(['add', '--verbose', msg.data.filename]); + lg.callMain(['commit', '-m', `edited ${msg.data.filename}`]); + FS.syncfs(false, () => { + console.log(currentRepoRootDir, 'stored to indexeddb'); + lg.callMain(['push']); + postMessage({ dircontents: FS.readdir('.') }); + }); + } else if (msg.data.command === 'writefile') { + FS.writeFile(msg.data.filename, msg.data.contents); + FS.syncfs(false, () => { + console.log(currentRepoRootDir, 'stored to indexeddb'); + postMessage({ dircontents: FS.readdir('.') }); + }); + } else if (msg.data.command === 'synclocal') { + currentRepoRootDir = msg.data.url.substring(msg.data.url.lastIndexOf('/') + 1); + console.log('synclocal', currentRepoRootDir); - let currentRepoRootDir; + FS.mkdir(`/${currentRepoRootDir}`); + FS.mount(IDBFS, {}, `/${currentRepoRootDir}`); - onmessage = (msg) => { - if (msg.data.command === 'writecommitandpush') { - FS.writeFile(msg.data.filename, msg.data.contents); - lg.callMain(['add', '--verbose', msg.data.filename]); - lg.callMain(['commit','-m', `edited ${msg.data.filename}`]); - FS.syncfs(false, () => { - console.log(currentRepoRootDir, 'stored to indexeddb'); - lg.callMain(['push']); + FS.syncfs(true, () => { + if (FS.readdir(`/${currentRepoRootDir}`).find(file => file === '.git')) { + FS.chdir(`/${currentRepoRootDir}`); postMessage({ dircontents: FS.readdir('.') }); - }); - } else if (msg.data.command === 'synclocal') { - currentRepoRootDir = msg.data.url.substring(msg.data.url.lastIndexOf('/') + 1); - console.log('synclocal', currentRepoRootDir); + console.log(currentRepoRootDir, 'restored from indexeddb'); + } else if (msg.data.newrepo) { + FS.chdir(`/${currentRepoRootDir}`); + postMessage({ empty: true }); + } else { + FS.chdir('/'); + postMessage({ notfound: true }); + } + }); + } else if (msg.data.command === 'deletelocal') { + FS.unmount(`/${currentRepoRootDir}`); + self.indexedDB.deleteDatabase('/' + currentRepoRootDir); + postMessage({ deleted: currentRepoRootDir }); + } else if (msg.data.command === 'dir') { + postMessage({ dircontents: FS.readdir('.') }); + } else if (msg.data.command === 'clone') { + currentRepoRootDir = msg.data.url.substring(msg.data.url.lastIndexOf('/') + 1); - FS.mkdir(`/${currentRepoRootDir}`); - FS.mount(IDBFS, { }, `/${currentRepoRootDir}`); - - FS.syncfs(true, () => { - - if( FS.readdir(`/${currentRepoRootDir}`).find(file => file === '.git') ) { - FS.chdir( `/${currentRepoRootDir}` ); - postMessage({ dircontents: FS.readdir('.') }); - console.log(currentRepoRootDir, 'restored from indexeddb'); - } else { - FS.chdir( '/' ); - postMessage('no git repo in ' + currentRepoRootDir); - postMessage({ notfound: true }); - } - }); - } else if (msg.data.command === 'deletelocal') { - postMessage('deleting database ' + currentRepoRootDir); - FS.unmount(`/${currentRepoRootDir}`); - postMessage('unmount done'); - self.indexedDB.deleteDatabase('/' + currentRepoRootDir); - postMessage('deleted from indexeddb'); - postMessage({ deleted: currentRepoRootDir}); - } else if (msg.data.command === 'clone') { - currentRepoRootDir = msg.data.url.substring(msg.data.url.lastIndexOf('/') + 1); - - postMessage({stdout: `git clone ${msg.data.url}`}); - lg.callMain(['clone', msg.data.url, currentRepoRootDir]); - FS.chdir(currentRepoRootDir); + lg.callMain(['clone', msg.data.url, currentRepoRootDir]); + FS.chdir(currentRepoRootDir); - FS.syncfs(false, () => { - console.log(currentRepoRootDir, 'stored to indexeddb'); - }); + FS.syncfs(false, () => { + console.log(currentRepoRootDir, 'stored to indexeddb'); postMessage({ dircontents: FS.readdir('.') }); - } else if (msg.data.command === 'pull') { - lg.callMain(['fetch', 'origin']); - lg.callMain(['merge', 'origin/master']); - FS.syncfs(false, () => { - console.log(currentRepoRootDir, 'stored to indexeddb'); + }); + } else if (msg.data.command === 'pull') { + lg.callMain(['fetch', 'origin']); + lg.callMain(['merge', 'origin/master']); + FS.syncfs(false, () => { + console.log(currentRepoRootDir, 'stored to indexeddb'); + }); + } else if (msg.data.command === 'readfile') { + try { + postMessage({ + filename: msg.data.filename, + filecontents: FS.readFile(msg.data.filename, { encoding: 'utf8' }) }); - } else if (msg.data.command === 'readfile') { - try { - postMessage({ - filename: msg.data.filename, - filecontents: FS.readFile(msg.data.filename, {encoding: 'utf8'}) - }); - } catch (e) { - postMessage({'stderr': JSON.stringify(e)}); - } - } else { - lg.callMain([msg.data.command]); + } catch (e) { + postMessage({ 'stderr': JSON.stringify(e) }); } - }; + } else { + const args = msg.data.args || []; + lg.callMain([msg.data.command, ...args]); + postMessage({ stdout: stdout.join('\n'), stderr: stderr.join('\n'), }); + } + + }; - postMessage({'ready': true}); + postMessage({ 'ready': true }); }; \ No newline at end of file diff --git a/test/checkout.spec.js b/test/checkout.spec.js index 710f063a..cb5fc32d 100644 --- a/test/checkout.spec.js +++ b/test/checkout.spec.js @@ -1,7 +1,11 @@ const lgPromise = require('./common.js').lgPromise; const assert = require('assert'); -describe('git checkout', () => { +describe('git checkout', () => { + beforeEach(async () => { + (await lgPromise).FS.chdir('/working'); + console.log('cwd', (await lgPromise).FS.cwd()); + }); it('should discard changes to a path', async () => { const lg = await lgPromise; const FS = lg.FS; @@ -31,8 +35,8 @@ describe('git checkout', () => { 'name = Test User\n' + 'email = test@example.com'); - FS.mkdir('test'); - FS.chdir('test'); + FS.mkdir('test99'); + FS.chdir('test99'); lg.callMain(['init', '.']); FS.writeFile('test.txt', 'abcdef'); diff --git a/test/conflict.spec.js b/test/conflict.spec.js new file mode 100644 index 00000000..ce5e1cfb --- /dev/null +++ b/test/conflict.spec.js @@ -0,0 +1,64 @@ +const lgPromise = require('./common.js').lgPromise; +const assert = require('assert'); + +describe('conflicts', function() { + beforeEach(async () => { + (await lgPromise).FS.chdir('/working'); + console.log('cwd', (await lgPromise).FS.cwd()); + }); + it('should create 1 bare and 2 clones and create/resolve conflicts', async () => { + const lg = await lgPromise; + const FS = lg.FS; + + lg.callMain(['config', 'user.name', 'The Tester']); + lg.callMain(['config', 'user.email', 'test@testing.com']); + FS.mkdir('bareconflicts'); + FS.chdir('bareconflicts'); + lg.callMain(['init', '--bare', '.']); + + FS.chdir('..'); + lg.callMain(['clone', 'bareconflicts', 'testconflicts1']); + + FS.chdir('testconflicts1'); + FS.writeFile('test.txt', 'abcdef'); + lg.callMain(['add', 'test.txt']); + lg.callMain(['commit', '-m', 'test commit 1']); + lg.callMain(['push']); + FS.chdir('..'); + + lg.callMain(['clone', 'bareconflicts', 'testconflicts2']); + + FS.chdir('testconflicts1'); + FS.writeFile('test.txt', 'abc'); + lg.callMain(['add', 'test.txt']); + lg.callMain(['commit', '-m', 'test commit 2']); + lg.callMain(['push']); + FS.chdir('..'); + + FS.chdir('testconflicts2'); + FS.writeFile('test.txt', 'hijklmn'); + lg.callMain(['add', 'test.txt']); + lg.callMain(['commit', '-m', 'test commit 3']); + + try { + lg.callWithOutput(['push']); + assert.fail('should reject pushing'); + } catch(e) { + assert.match(e, /cannot push because a reference that you are trying to update on the remote contains commits that are not present locally/) + } + + lg.callMain(['fetch','origin']); + lg.callMain(['merge', 'origin/master']); + + assert.match(lg.callWithOutput(['status']), /conflict\: a\:test\.txt o\:test\.txt t\:test\.txt/); + + FS.writeFile('test.txt', 'abcxyz'); + lg.callMain(['add', 'test.txt']); + lg.callMain(['commit', '-m', 'resolved conflict']); + + assert.match(lg.callWithOutput(['log']), /resolved conflict/); + assert.match(lg.callWithOutput(['push']),/pushed/); + console.log('status', lg.callWithOutput(['status'])); + assert.equal('abcxyz', FS.readFile('test.txt', {encoding: 'utf8'})); + }); +}); \ No newline at end of file diff --git a/test/fetch.spec.js b/test/fetch.spec.js index 72480d0d..ecf8f766 100644 --- a/test/fetch.spec.js +++ b/test/fetch.spec.js @@ -2,9 +2,16 @@ const assert = require('assert'); const lgPromise = require('./common.js').lgPromise; describe('git fetch', () => { + beforeEach(async () => { + (await lgPromise).FS.chdir('/working'); + console.log('cwd', (await lgPromise).FS.cwd()); + }); it('should create 1 bare and 2 clones and fetch changes', async () => { const lg = await lgPromise; const FS = lg.FS; + lg.callMain(['config', 'user.name', 'The Tester']); + lg.callMain(['config', 'user.email', 'test@testing.com']); + FS.mkdir('bare'); FS.chdir('bare'); lg.callMain(['init', '--bare', '.']); @@ -38,5 +45,23 @@ describe('git fetch', () => { const result = lg.callWithOutput(['log']); assert.ok(result.indexOf('test commit 2') > 0); assert.ok(result.indexOf('test commit 1') > result.indexOf('test commit 2') > 0); + + lg.callMain(['checkout', '-b', 'testbranch']); + FS.writeFile('testinbranch.txt', 'abcdef'); + lg.callMain(['add', 'testinbranch.txt']); + lg.callMain(['commit', '-m', 'test in branch']); + lg.callMain(['push']); + + FS.chdir('..'); + + FS.chdir('test2'); + + assert.equal(FS.analyzePath('testinbranch.txt').exists, false); + lg.callMain(['fetch', 'origin']); + lg.callMain(['checkout', 'testbranch']); + + assert.match(lg.callWithOutput(['status']), /On branch testbranch/); + assert.equal(FS.analyzePath('testinbranch.txt').exists, true); + assert.equal(FS.readFile('testinbranch.txt', {encoding: 'utf8'}), 'abcdef'); }); }); diff --git a/test/nodefs.spec.js b/test/nodefs.spec.js index 40203b72..09b7f031 100644 --- a/test/nodefs.spec.js +++ b/test/nodefs.spec.js @@ -2,7 +2,7 @@ const lgPromise = require('./common.js').lgPromise; const assert = require('assert'); const { rmSync } = require('fs'); -describe.only('nodefs', function () { +describe('nodefs', function () { this.timeout(20000); it('should clone using nodefs', async () => {