diff options
-rw-r--r-- | include/git2/errors.h | 1 | ||||
-rw-r--r-- | include/git2/submodule.h | 465 | ||||
-rw-r--r-- | src/config_file.c | 6 | ||||
-rw-r--r-- | src/config_file.h | 12 | ||||
-rw-r--r-- | src/diff.c | 2 | ||||
-rw-r--r-- | src/submodule.c | 1381 | ||||
-rw-r--r-- | src/submodule.h | 94 | ||||
-rw-r--r-- | tests-clar/status/submodules.c | 16 | ||||
-rw-r--r-- | tests-clar/submodule/lookup.c | 110 | ||||
-rw-r--r-- | tests-clar/submodule/modify.c | 256 | ||||
-rw-r--r-- | tests-clar/submodule/status.c | 44 | ||||
-rw-r--r-- | tests-clar/submodule/submodule_helpers.c | 84 | ||||
-rw-r--r-- | tests-clar/submodule/submodule_helpers.h | 2 |
13 files changed, 2273 insertions, 200 deletions
diff --git a/include/git2/errors.h b/include/git2/errors.h index 2ab1da403..b55f8c30d 100644 --- a/include/git2/errors.h +++ b/include/git2/errors.h @@ -54,6 +54,7 @@ typedef enum { GITERR_TREE, GITERR_INDEXER, GITERR_SSL, + GITERR_SUBMODULE, } git_error_t; /** diff --git a/include/git2/submodule.h b/include/git2/submodule.h index f65911a3b..6cd66465e 100644 --- a/include/git2/submodule.h +++ b/include/git2/submodule.h @@ -20,54 +20,169 @@ */ GIT_BEGIN_DECL +/** + * Opaque structure representing a submodule. + * + * Submodule support in libgit2 builds a list of known submodules and keeps + * it in the repository. The list is built from the .gitmodules file, the + * .git/config file, the index, and the HEAD tree. Items in the working + * directory that look like submodules (i.e. a git repo) but are not + * mentioned in those places won't be tracked. + */ +typedef struct git_submodule git_submodule; + +/** + * Values that could be specified for the update rule of a submodule. + * + * Use the DEFAULT value if you have altered the update value via + * `git_submodule_set_update()` and wish to reset to the original default. + */ typedef enum { + GIT_SUBMODULE_UPDATE_DEFAULT = -1, GIT_SUBMODULE_UPDATE_CHECKOUT = 0, GIT_SUBMODULE_UPDATE_REBASE = 1, - GIT_SUBMODULE_UPDATE_MERGE = 2 + GIT_SUBMODULE_UPDATE_MERGE = 2, + GIT_SUBMODULE_UPDATE_NONE = 3 } git_submodule_update_t; +/** + * Values that could be specified for how closely to examine the + * working directory when getting submodule status. + * + * Use the DEFUALT value if you have altered the ignore value via + * `git_submodule_set_ignore()` and wish to reset to the original value. + */ typedef enum { - GIT_SUBMODULE_IGNORE_ALL = 0, /* never dirty */ - GIT_SUBMODULE_IGNORE_DIRTY = 1, /* only dirty if HEAD moved */ - GIT_SUBMODULE_IGNORE_UNTRACKED = 2, /* dirty if tracked files change */ - GIT_SUBMODULE_IGNORE_NONE = 3 /* any change or untracked == dirty */ + GIT_SUBMODULE_IGNORE_DEFAULT = -1, /* reset to default */ + GIT_SUBMODULE_IGNORE_NONE = 0, /* any change or untracked == dirty */ + GIT_SUBMODULE_IGNORE_UNTRACKED = 1, /* dirty if tracked files change */ + GIT_SUBMODULE_IGNORE_DIRTY = 2, /* only dirty if HEAD moved */ + GIT_SUBMODULE_IGNORE_ALL = 3 /* never dirty */ } git_submodule_ignore_t; /** - * Description of submodule + * Status values for submodules. + * + * One of these values will be returned for the submodule in the index + * relative to the HEAD tree, and one will be returned for the submodule in + * the working directory relative to the index. The value can be extracted + * from the actual submodule status return value using one of the macros + * below (see GIT_SUBMODULE_INDEX_STATUS and GIT_SUBMODULE_WD_STATUS). + */ +enum { + GIT_SUBMODULE_STATUS_CLEAN = 0, + GIT_SUBMODULE_STATUS_ADDED = 1, + GIT_SUBMODULE_STATUS_REMOVED = 2, + GIT_SUBMODULE_STATUS_REMOVED_TYPE_CHANGE = 3, + GIT_SUBMODULE_STATUS_MODIFIED = 4, + GIT_SUBMODULE_STATUS_MODIFIED_AHEAD = 5, + GIT_SUBMODULE_STATUS_MODIFIED_BEHIND = 6 +}; + +/** + * Return codes for submodule status. + * + * A combination of these flags (and shifted values of the + * GIT_SUBMODULE_STATUS codes above) will be returned to describe the status + * of a submodule. + * + * Submodule info is contained in 4 places: the HEAD tree, the index, config + * files (both .git/config and .gitmodules), and the working directory. Any + * or all of those places might be missing information about the submodule + * depending on what state the repo is in. + * + * When you ask for submodule status, we consider all four places and return + * a combination of the flags below. Also, we also compare HEAD to index to + * workdir, and return a relative status code (see above) for the + * comparisons. Use the GIT_SUBMODULE_INDEX_STATUS() and + * GIT_SUBMODULE_WD_STATUS() macros to extract these status codes from the + * results. As an example, if the submodule exists in the HEAD and does not + * exist in the index, then using GIT_SUBMODULE_INDEX_STATUS(st) will return + * GIT_SUBMODULE_STATUS_REMOVED. + * + * The ignore settings for the submodule will control how much status info + * you get about the working directory. For example, with ignore ALL, the + * workdir will always show as clean. With any ignore level below NONE, + * you will never get the WD_HAS_UNTRACKED value back. + * + * The other SUBMODULE_STATUS values you might see are: + * + * - IN_HEAD means submodule exists in HEAD tree + * - IN_INDEX means submodule exists in index + * - IN_CONFIG means submodule exists in config + * - IN_WD means submodule exists in workdir and looks like a submodule + * - WD_CHECKED_OUT means submodule in workdir has .git content + * - WD_HAS_UNTRACKED means workdir contains untracked files. This would + * only ever be returned for ignore value GIT_SUBMODULE_IGNORE_NONE. + * - WD_MISSING_COMMITS means workdir repo is out of date and does not + * contain the SHAs from either the index or the HEAD tree + */ +#define GIT_SUBMODULE_STATUS_IN_HEAD (1u << 0) +#define GIT_SUBMODULE_STATUS_IN_INDEX (1u << 1) +#define GIT_SUBMODULE_STATUS_IN_CONFIG (1u << 2) +#define GIT_SUBMODULE_STATUS_IN_WD (1u << 3) +#define GIT_SUBMODULE_STATUS_INDEX_DATA_OFFSET (4) +#define GIT_SUBMODULE_STATUS_WD_DATA_OFFSET (7) +#define GIT_SUBMODULE_STATUS_WD_CHECKED_OUT (1u << 10) +#define GIT_SUBMODULE_STATUS_WD_HAS_UNTRACKED (1u << 11) +#define GIT_SUBMODULE_STATUS_WD_MISSING_COMMITS (1u << 12) + +/** + * Extract submodule status value for index from status mask. + */ +#define GIT_SUBMODULE_INDEX_STATUS(s) \ + (((s) >> GIT_SUBMODULE_STATUS_INDEX_DATA_OFFSET) & 0x07) + +/** + * Extract submodule status value for working directory from status mask. + */ +#define GIT_SUBMODULE_WD_STATUS(s) \ + (((s) >> GIT_SUBMODULE_STATUS_WD_DATA_OFFSET) & 0x07) + +/** + * Lookup submodule information by name or path. + * + * Given either the submodule name or path (they are usually the same), this + * returns a structure describing the submodule. + * + * There are two expected error scenarios: * - * This record describes a submodule found in a repository. There - * should be an entry for every submodule found in the HEAD and for - * every submodule described in .gitmodules. The fields are as follows: + * - The submodule is not mentioned in the HEAD, the index, and the config, + * but does "exist" in the working directory (i.e. there is a subdirectory + * that is a valid self-contained git repo). In this case, this function + * returns GIT_EEXISTS to indicate the the submodule exists but not in a + * state where a git_submodule can be instantiated. + * - The submodule is not mentioned in the HEAD, index, or config and the + * working directory doesn't contain a value git repo at that path. + * There may or may not be anything else at that path, but nothing that + * looks like a submodule. In this case, this returns GIT_ENOTFOUND. * - * - `name` is the name of the submodule from .gitmodules. - * - `path` is the path to the submodule from the repo working directory. - * It is almost always the same as `name`. - * - `url` is the url for the submodule. - * - `oid` is the HEAD SHA1 for the submodule. - * - `update` is a value from above - see gitmodules(5) update. - * - `ignore` is a value from above - see gitmodules(5) ignore. - * - `fetch_recurse` is 0 or 1 - see gitmodules(5) fetchRecurseSubmodules. - * - `refcount` is for internal use. + * The submodule object is owned by the containing repo and will be freed + * when the repo is freed. The caller need not free the submodule. * - * If the submodule has been added to .gitmodules but not yet git added, - * then the `oid` will be zero. If the submodule has been deleted, but - * the delete has not been committed yet, then the `oid` will be set, but - * the `url` will be NULL. + * @param submodule Pointer to submodule description object pointer.. + * @param repo The repository. + * @param name The name of the submodule. Trailing slashes will be ignored. + * @return 0 on success, GIT_ENOTFOUND if submodule does not exist, + * GIT_EEXISTS if submodule exists in working directory only, -1 on + * other errors. */ -typedef struct { - char *name; - char *path; - char *url; - git_oid oid; /* sha1 of submodule HEAD ref or zero if not committed */ - git_submodule_update_t update; - git_submodule_ignore_t ignore; - int fetch_recurse; - int refcount; -} git_submodule; +GIT_EXTERN(int) git_submodule_lookup( + git_submodule **submodule, + git_repository *repo, + const char *name); /** - * Iterate over all submodules of a repository. + * Iterate over all tracked submodules of a repository. + * + * See the note on `git_submodule` above. This iterates over the tracked + * submodules as decribed therein. + * + * If you are concerned about items in the working directory that look like + * submodules but are not tracked, the diff API will generate a diff record + * for workdir items that look like submodules but are not tracked, showing + * them as added in the workdir. Also, the status API will treat the entire + * subdirectory of a contained git repo as a single GIT_STATUS_WT_NEW item. * * @param repo The repository * @param callback Function to be called with the name of each submodule. @@ -77,26 +192,286 @@ typedef struct { */ GIT_EXTERN(int) git_submodule_foreach( git_repository *repo, - int (*callback)(const char *name, void *payload), + int (*callback)(git_submodule *sm, const char *name, void *payload), void *payload); /** - * Lookup submodule information by name or path. + * Set up a new git submodule for checkout. * - * Given either the submodule name or path (they are usually the same), - * this returns a structure describing the submodule. If the submodule - * does not exist, this will return GIT_ENOTFOUND and set the submodule - * pointer to NULL. + * This does "git submodule add" up to the fetch and checkout of the + * submodule contents. It preps a new submodule, creates an entry in + * .gitmodules and creates an empty initialized repository either at the + * given path in the working directory or in .git/modules with a gitlink + * from the working directory to the new repo. * - * @param submodule Pointer to submodule description object pointer.. - * @param repo The repository. - * @param name The name of the submodule. Trailing slashes will be ignored. - * @return 0 on success, GIT_ENOTFOUND if submodule does not exist, -1 on error + * To fully emulate "git submodule add" call this function, then open the + * submodule repo and perform the clone step as needed. Lastly, call + * `git_submodule_add_finalize` to wrap up adding the new submodule and + * .gitmodules to the index to be ready to commit. + * + * @param submodule The newly created submodule ready to open for clone + * @param repo Superproject repository to contain the new submodule + * @param url URL for the submodules remote + * @param path Path at which the submodule should be created + * @param use_gitlink Should workdir contain a gitlink to the repo in + * .git/modules vs. repo directly in workdir. + * @return 0 on success, GIT_EEXISTS if submodule already exists, + * -1 on other errors. */ -GIT_EXTERN(int) git_submodule_lookup( +GIT_EXTERN(int) git_submodule_add_setup( git_submodule **submodule, git_repository *repo, - const char *name); + const char *url, + const char *path, + int use_gitlink); + +/** + * Resolve the setup of a new git submodule. + * + * This should be called on a submodule once you have called add setup + * and done the clone of the submodule. This adds the .gitmodules file + * and the newly cloned submodule to the index to be ready to be committed + * (but doesn't actually do the commit). + */ +GIT_EXTERN(int) git_submodule_add_finalize(git_submodule *submodule); + +/** + * Add current submodule HEAD commit to index of superproject. + */ +GIT_EXTERN(int) git_submodule_add_to_index(git_submodule *submodule); + +/** + * Write submodule settings to .gitmodules file. + * + * This commits any in-memory changes to the submodule to the gitmodules + * file on disk. You may also be interested in `git_submodule_init` which + * writes submodule info to ".git/config" (which is better for local changes + * to submodule settings) and/or `git_submodule_sync` which writes settings + * about remotes to the actual submodule repository. + * + * @param submodule The submodule to write. + * @return 0 on success, <0 on failure. + */ +GIT_EXTERN(int) git_submodule_save(git_submodule *submodule); + +/** + * Get the containing repository for a submodule. + * + * This returns a pointer to the repository that contains the submodule. + * This is a just a reference to the repository that was passed to the + * original `git_submodule_lookup` call, so if that repository has been + * freed, then this may be a dangling reference. + * + * @param submodule Pointer to submodule object + * @return Pointer to `git_repository` + */ +GIT_EXTERN(git_repository *) git_submodule_owner(git_submodule *submodule); + +/** + * Get the name of submodule. + * + * @param submodule Pointer to submodule object + * @return Pointer to the submodule name + */ +GIT_EXTERN(const char *) git_submodule_name(git_submodule *submodule); + +/** + * Get the path to the submodule. + * + * The path is almost always the same as the submodule name, but the + * two are actually not required to match. + * + * @param submodule Pointer to submodule object + * @return Pointer to the submodule path + */ +GIT_EXTERN(const char *) git_submodule_path(git_submodule *submodule); + +/** + * Get the URL for the submodule. + * + * @param submodule Pointer to submodule object + * @return Pointer to the submodule url + */ +GIT_EXTERN(const char *) git_submodule_url(git_submodule *submodule); + +/** + * Set the URL for the submodule. + * + * This sets the URL in memory for the submodule. This will be used for + * any following submodule actions while this submodule data is in memory. + * + * After calling this, you may wish to call `git_submodule_save` to write + * the changes back to the ".gitmodules" file and `git_submodule_sync` to + * write the changes to the checked out submodule repository. + * + * @param submodule Pointer to the submodule object + * @param url URL that should be used for the submodule + * @return 0 on success, <0 on failure + */ +GIT_EXTERN(int) git_submodule_set_url(git_submodule *submodule, const char *url); + +/** + * Get the OID for the submodule in the index. + * + * @param submodule Pointer to submodule object + * @return Pointer to git_oid or NULL if submodule is not in index. + */ +GIT_EXTERN(const git_oid *) git_submodule_index_oid(git_submodule *submodule); + +/** + * Get the OID for the submodule in the current HEAD tree. + * + * @param submodule Pointer to submodule object + * @return Pointer to git_oid or NULL if submodule is not in the HEAD. + */ +GIT_EXTERN(const git_oid *) git_submodule_head_oid(git_submodule *submodule); + +/** + * Get the OID for the submodule in the current working directory. + * + * This returns the OID that corresponds to looking up 'HEAD' in the checked + * out submodule. If there are pending changes in the index or anything + * else, this won't notice that. You should call `git_submodule_status` for + * a more complete picture about the state of the working directory. + * + * @param submodule Pointer to submodule object + * @return Pointer to git_oid or NULL if submodule is not checked out. + */ +GIT_EXTERN(const git_oid *) git_submodule_wd_oid(git_submodule *submodule); + +/** + * Get the ignore rule for the submodule. + * + * There are four ignore values: + * + * - **GIT_SUBMODULE_IGNORE_NONE** will consider any change to the contents + * of the submodule from a clean checkout to be dirty, including the + * addition of untracked files. This is the default if unspecified. + * - **GIT_SUBMODULE_IGNORE_UNTRACKED** examines the contents of the + * working tree (i.e. call `git_status_foreach` on the submodule) but + * UNTRACKED files will not count as making the submodule dirty. + * - **GIT_SUBMODULE_IGNORE_DIRTY** means to only check if the HEAD of the + * submodule has moved for status. This is fast since it does not need to + * scan the working tree of the submodule at all. + * - **GIT_SUBMODULE_IGNORE_ALL** means not to open the submodule repo. + * The working directory will be consider clean so long as there is a + * checked out version present. + */ +GIT_EXTERN(git_submodule_ignore_t) git_submodule_ignore( + git_submodule *submodule); + +/** + * Set the ignore rule for the submodule. + * + * This sets the ignore rule in memory for the submodule. This will be used + * for any following actions (such as `git_submodule_status`) while the + * submodule is in memory. You should call `git_submodule_save` if you want + * to persist the new ignore role. + * + * Calling this again with GIT_SUBMODULE_IGNORE_DEFAULT or calling + * `git_submodule_reload` will revert the rule to the value that was in the + * original config. + * + * @return old value for ignore + */ +GIT_EXTERN(git_submodule_ignore_t) git_submodule_set_ignore( + git_submodule *submodule, + git_submodule_ignore_t ignore); + +/** + * Get the update rule for the submodule. + */ +GIT_EXTERN(git_submodule_update_t) git_submodule_update( + git_submodule *submodule); + +/** + * Set the update rule for the submodule. + * + * This sets the update rule in memory for the submodule. You should call + * `git_submodule_save` if you want to persist the new update rule. + * + * Calling this again with GIT_SUBMODULE_UPDATE_DEFAULT or calling + * `git_submodule_reload` will revert the rule to the value that was in the + * original config. + * + * @return old value for update + */ +GIT_EXTERN(git_submodule_update_t) git_submodule_set_update( + git_submodule *submodule, + git_submodule_update_t update); + +/** + * Copy submodule info into ".git/config" file. + * + * Just like "git submodule init", this copies information about the + * submodule into ".git/config". You can use the accessor functions + * above to alter the in-memory git_submodule object and control what + * is written to the config, overriding what is in .gitmodules. + * + * @param submodule The submodule to write into the superproject config + * @param overwrite By default, existing entries will not be overwritten, + * but setting this to true forces them to be updated. + * @return 0 on success, <0 on failure. + */ +GIT_EXTERN(int) git_submodule_init(git_submodule *submodule, int overwrite); + +/** + * Copy submodule remote info into submodule repo. + * + * This copies the information about the submodules URL into the checked out + * submodule config, acting like "git submodule sync". This is useful if + * you have altered the URL for the submodule (or it has been altered by a + * fetch of upstream changes) and you need to update your local repo. + */ +GIT_EXTERN(int) git_submodule_sync(git_submodule *submodule); + +/** + * Open the repository for a submodule. + * + * This is a newly opened repository object. The caller is responsible for + * calling `git_repository_free` on it when done. Multiple calls to this + * function will return distinct `git_repository` objects. This will only + * work if the submodule is checked out into the working directory. + * + * @param subrepo Pointer to the submodule repo which was opened + * @param submodule Submodule to be opened + * @return 0 on success, <0 if submodule repo could not be opened. + */ +GIT_EXTERN(int) git_submodule_open( + git_repository **repo, + git_submodule *submodule); + +/** + * Reread submodule info from config, index, and HEAD. + * + * Call this to reread cached submodule information for this submodule if + * you have reason to believe that it has changed. + */ +GIT_EXTERN(int) git_submodule_reload(git_submodule *submodule); + +/** + * Reread all submodule info. + * + * Call this to reload all cached submodule information for the repo. + */ +GIT_EXTERN(int) git_submodule_reload_all(git_repository *repo); + +/** + * Get the status for a submodule. + * + * This looks at a submodule and tries to determine the status. It + * will return a combination of the `GIT_SUBMODULE_STATUS` values above. + * How deeply it examines the working directory to do this will depend + * on the `git_submodule_ignore_t` value for the submodule (which can be + * overridden with `git_submodule_set_ignore()`). + * + * @param status Combination of GIT_SUBMODULE_STATUS values from above. + * @param submodule Submodule for which to get status + * @return 0 on success, <0 on error + */ +GIT_EXTERN(int) git_submodule_status( + unsigned int *status, + git_submodule *submodule); /** @} */ GIT_END_DECL diff --git a/src/config_file.c b/src/config_file.c index 547509b9f..aabb21f16 100644 --- a/src/config_file.c +++ b/src/config_file.c @@ -253,11 +253,17 @@ static int config_set(git_config_file *cfg, const char *name, const char *value) char *tmp = NULL; git__free(key); + if (existing->next != NULL) { giterr_set(GITERR_CONFIG, "Multivar incompatible with simple set"); return -1; } + /* don't update if old and new values already match */ + if ((!existing->value && !value) || + (existing->value && value && !strcmp(existing->value, value))) + return 0; + if (value) { tmp = git__strdup(value); GITERR_CHECK_ALLOC(tmp); diff --git a/src/config_file.h b/src/config_file.h index c31292881..bf687b516 100644 --- a/src/config_file.h +++ b/src/config_file.h @@ -19,12 +19,24 @@ GIT_INLINE(void) git_config_file_free(git_config_file *cfg) cfg->free(cfg); } +GIT_INLINE(int) git_config_file_get_string( + const char **out, git_config_file *cfg, const char *name) +{ + return cfg->get(cfg, name, out); +} + GIT_INLINE(int) git_config_file_set_string( git_config_file *cfg, const char *name, const char *value) { return cfg->set(cfg, name, value); } +GIT_INLINE(int) git_config_file_delete( + git_config_file *cfg, const char *name) +{ + return cfg->del(cfg, name); +} + GIT_INLINE(int) git_config_file_foreach( git_config_file *cfg, int (*fn)(const char *key, const char *value, void *data), diff --git a/src/diff.c b/src/diff.c index 9abf8b9f5..430f52e0a 100644 --- a/src/diff.c +++ b/src/diff.c @@ -530,7 +530,7 @@ static int maybe_modified( status = GIT_DELTA_UNMODIFIED; else if (git_submodule_lookup(&sub, diff->repo, nitem->path) < 0) return -1; - else if (sub->ignore == GIT_SUBMODULE_IGNORE_ALL) + else if (git_submodule_ignore(sub) == GIT_SUBMODULE_IGNORE_ALL) status = GIT_DELTA_UNMODIFIED; else { /* TODO: support other GIT_SUBMODULE_IGNORE values */ diff --git a/src/submodule.c b/src/submodule.c index b8537cb8c..9a852041a 100644 --- a/src/submodule.c +++ b/src/submodule.c @@ -17,18 +17,24 @@ #include "config_file.h" #include "config.h" #include "repository.h" +#include "submodule.h" +#include "tree.h" +#include "iterator.h" + +#define GIT_MODULES_FILE ".gitmodules" static git_cvar_map _sm_update_map[] = { {GIT_CVAR_STRING, "checkout", GIT_SUBMODULE_UPDATE_CHECKOUT}, {GIT_CVAR_STRING, "rebase", GIT_SUBMODULE_UPDATE_REBASE}, - {GIT_CVAR_STRING, "merge", GIT_SUBMODULE_UPDATE_MERGE} + {GIT_CVAR_STRING, "merge", GIT_SUBMODULE_UPDATE_MERGE}, + {GIT_CVAR_STRING, "none", GIT_SUBMODULE_UPDATE_NONE}, }; static git_cvar_map _sm_ignore_map[] = { - {GIT_CVAR_STRING, "all", GIT_SUBMODULE_IGNORE_ALL}, - {GIT_CVAR_STRING, "dirty", GIT_SUBMODULE_IGNORE_DIRTY}, + {GIT_CVAR_STRING, "none", GIT_SUBMODULE_IGNORE_NONE}, {GIT_CVAR_STRING, "untracked", GIT_SUBMODULE_IGNORE_UNTRACKED}, - {GIT_CVAR_STRING, "none", GIT_SUBMODULE_IGNORE_NONE} + {GIT_CVAR_STRING, "dirty", GIT_SUBMODULE_IGNORE_DIRTY}, + {GIT_CVAR_STRING, "all", GIT_SUBMODULE_IGNORE_ALL}, }; static kh_inline khint_t str_hash_no_trailing_slash(const char *s) @@ -55,9 +61,725 @@ static kh_inline int str_equal_no_trailing_slash(const char *a, const char *b) return (alen == blen && strncmp(a, b, alen) == 0); } -__KHASH_IMPL(str, static kh_inline, const char *, void *, 1, str_hash_no_trailing_slash, str_equal_no_trailing_slash); +__KHASH_IMPL( + str, static kh_inline, const char *, void *, 1, + str_hash_no_trailing_slash, str_equal_no_trailing_slash); + +static int load_submodule_config( + git_repository *repo, bool force); +static git_config_file *open_gitmodules( + git_repository *, bool, const git_oid *); +static int lookup_head_remote( + git_buf *url, git_repository *repo); +static git_submodule *submodule_lookup_or_create( + git_repository *repo, const char *n1, const char *n2); +static int submodule_update_map( + git_repository *repo, git_submodule *sm, const char *key); +static void submodule_release( + git_submodule *sm, int decr); +static int submodule_load_from_index( + git_repository *, const git_index_entry *); +static int submodule_load_from_head( + git_repository *, const char *, const git_oid *); +static int submodule_load_from_config( + const char *, const char *, void *); +static int submodule_update_config( + git_submodule *, const char *, const char *, bool, bool); + +static int submodule_cmp(const void *a, const void *b) +{ + return strcmp(((git_submodule *)a)->name, ((git_submodule *)b)->name); +} + +static int submodule_config_key_trunc_puts(git_buf *key, const char *suffix) +{ + ssize_t idx = git_buf_rfind(key, '.'); + git_buf_truncate(key, (size_t)(idx + 1)); + return git_buf_puts(key, suffix); +} + +/* + * PUBLIC APIS + */ + +int git_submodule_lookup( + git_submodule **sm_ptr, /* NULL if user only wants to test existence */ + git_repository *repo, + const char *name) /* trailing slash is allowed */ +{ + int error; + khiter_t pos; + + assert(repo && name); + + if ((error = load_submodule_config(repo, false)) < 0) + return error; + + pos = git_strmap_lookup_index(repo->submodules, name); + + if (!git_strmap_valid_index(repo->submodules, pos)) { + error = GIT_ENOTFOUND; + + /* check if a plausible submodule exists at path */ + if (git_repository_workdir(repo)) { + git_buf path = GIT_BUF_INIT; + + if (git_buf_joinpath(&path, git_repository_workdir(repo), name) < 0) + return -1; + + if (git_path_contains_dir(&path, DOT_GIT)) + error = GIT_EEXISTS; + + git_buf_free(&path); + } + + return error; + } + + if (sm_ptr) + *sm_ptr = git_strmap_value_at(repo->submodules, pos); + + return 0; +} + +int git_submodule_foreach( + git_repository *repo, + int (*callback)(git_submodule *sm, const char *name, void *payload), + void *payload) +{ + int error; + git_submodule *sm; + git_vector seen = GIT_VECTOR_INIT; + seen._cmp = submodule_cmp; + + assert(repo && callback); + + if ((error = load_submodule_config(repo, false)) < 0) + return error; + + git_strmap_foreach_value(repo->submodules, sm, { + /* Usually the following will not come into play - it just prevents + * us from issuing a callback twice for a submodule where the name + * and path are not the same. + */ + if (sm->refcount > 1) { + if (git_vector_bsearch(&seen, sm) != GIT_ENOTFOUND) + continue; + if ((error = git_vector_insert(&seen, sm)) < 0) + break; + } + + if ((error = callback(sm, sm->name, payload)) < 0) + break; + }); + + git_vector_free(&seen); + + return error; +} + +void git_submodule_config_free(git_repository *repo) +{ + git_strmap *smcfg; + git_submodule *sm; + + assert(repo); + + smcfg = repo->submodules; + repo->submodules = NULL; + + if (smcfg == NULL) + return; + + git_strmap_foreach_value(smcfg, sm, { + submodule_release(sm,1); + }); + git_strmap_free(smcfg); +} + +int git_submodule_add_setup( + git_submodule **submodule, + git_repository *repo, + const char *url, + const char *path, + int use_gitlink) +{ + int error = 0; + git_config_file *mods = NULL; + git_submodule *sm; + git_buf name = GIT_BUF_INIT, real_url = GIT_BUF_INIT; + git_repository_init_options initopt; + git_repository *subrepo = NULL; + + assert(repo && url && path); + + /* see if there is already an entry for this submodule */ + + if (git_submodule_lookup(&sm, repo, path) < 0) + giterr_clear(); + else { + giterr_set(GITERR_SUBMODULE, + "Attempt to add a submodule that already exists"); + return GIT_EEXISTS; + } + + /* resolve parameters */ + + if (url[0] == '.' && (url[1] == '/' || (url[1] == '.' && url[2] == '/'))) { + if (!(error = lookup_head_remote(&real_url, repo))) + error = git_path_apply_relative(&real_url, url); + } else if (strchr(url, ':') != NULL || url[0] == '/') { + error = git_buf_sets(&real_url, url); + } else { + giterr_set(GITERR_SUBMODULE, "Invalid format for submodule URL"); + error = -1; + } + if (error) + goto cleanup; + + /* validate and normalize path */ + + if (git__prefixcmp(path, git_repository_workdir(repo)) == 0) + path += strlen(git_repository_workdir(repo)); + + if (git_path_root(path) >= 0) { + giterr_set(GITERR_SUBMODULE, "Submodule path must be a relative path"); + error = -1; + goto cleanup; + } + + /* update .gitmodules */ + + if ((mods = open_gitmodules(repo, true, NULL)) == NULL) { + giterr_set(GITERR_SUBMODULE, + "Adding submodules to a bare repository is not supported (for now)"); + return -1; + } + + if ((error = git_buf_printf(&name, "submodule.%s.path", path)) < 0 || + (error = git_config_file_set_string(mods, name.ptr, path)) < 0) + goto cleanup; + + if ((error = submodule_config_key_trunc_puts(&name, "url")) < 0 || + (error = git_config_file_set_string(mods, name.ptr, real_url.ptr)) < 0) + goto cleanup; + + git_buf_clear(&name); + + /* init submodule repository and add origin remote as needed */ + + error = git_buf_joinpath(&name, git_repository_workdir(repo), path); + if (error < 0) + goto cleanup; + + /* New style: sub-repo goes in <repo-dir>/modules/<name>/ with a + * gitlink in the sub-repo workdir directory to that repository + * + * Old style: sub-repo goes directly into repo/<name>/.git/ + */ + + memset(&initopt, 0, sizeof(initopt)); + initopt.flags = GIT_REPOSITORY_INIT_MKPATH | + GIT_REPOSITORY_INIT_NO_REINIT; + initopt.origin_url = real_url.ptr; + + if (git_path_exists(name.ptr) && + git_path_contains(&name, DOT_GIT)) + { + /* repo appears to already exist - reinit? */ + } + else if (use_gitlink) { + git_buf repodir = GIT_BUF_INIT; + + error = git_buf_join_n( + &repodir, '/', 3, git_repository_path(repo), "modules", path); + if (error < 0) + goto cleanup; + + initopt.workdir_path = name.ptr; + initopt.flags |= GIT_REPOSITORY_INIT_NO_DOTGIT_DIR; + + error = git_repository_init_ext(&subrepo, repodir.ptr, &initopt); + + git_buf_free(&repodir); + } + else { + error = git_repository_init_ext(&subrepo, name.ptr, &initopt); + } + if (error < 0) + goto cleanup; + + /* add submodule to hash and "reload" it */ + + if ((sm = submodule_lookup_or_create(repo, path, NULL)) == NULL) { + error = -1; + goto cleanup; + } + + if ((error = submodule_update_map(repo, sm, sm->path)) < 0) + goto cleanup; + + if ((error = git_submodule_reload(sm)) < 0) + goto cleanup; + + error = git_submodule_init(sm, false); + +cleanup: + if (submodule != NULL) + *submodule = !error ? sm : NULL; + + if (mods != NULL) + git_config_file_free(mods); + git_repository_free(subrepo); + git_buf_free(&real_url); + git_buf_free(&name); + + return error; +} + +int git_submodule_add_finalize(git_submodule *sm) +{ + int error; + git_index *index; + + assert(sm); + + if ((error = git_repository_index__weakptr(&index, sm->owner)) < 0 || + (error = git_index_add(index, GIT_MODULES_FILE, 0)) < 0) + return error; + + return git_submodule_add_to_index(sm); +} + +int git_submodule_add_to_index(git_submodule *sm) +{ + int error; + git_repository *repo, *sm_repo; + git_index *index; + git_buf path = GIT_BUF_INIT; + git_commit *head; + git_index_entry entry; + struct stat st; + + assert(sm); + + repo = sm->owner; + + if ((error = git_repository_index__weakptr(&index, repo)) < 0 || + (error = git_buf_joinpath( + &path, git_repository_workdir(repo), sm->path)) < 0 || + (error = git_submodule_open(&sm_repo, sm)) < 0) + goto cleanup; + + /* read stat information for submodule working directory */ + if (p_stat(path.ptr, &st) < 0) { + giterr_set(GITERR_SUBMODULE, + "Cannot add submodule without working directory"); + error = -1; + goto cleanup; + } + git_index__init_entry_from_stat(&st, &entry); + + /* calling git_submodule_open will have set sm->wd_oid if possible */ + if ((sm->flags & GIT_SUBMODULE_STATUS__WD_OID_VALID) == 0) { + giterr_set(GITERR_SUBMODULE, + "Cannot add submodule without HEAD to index"); + error = -1; + goto cleanup; + } + git_oid_cpy(&entry.oid, &sm->wd_oid); + + if ((error = git_commit_lookup(&head, sm_repo, &sm->wd_oid)) < 0) + goto cleanup; + + entry.ctime.seconds = git_commit_time(head); + entry.ctime.nanoseconds = 0; + entry.mtime.seconds = git_commit_time(head); + entry.mtime.nanoseconds = 0; -static git_submodule *submodule_alloc(const char *name) + git_commit_free(head); + + /* now add it */ + error = git_index_add2(index, &entry); + +cleanup: + git_repository_free(sm_repo); + git_buf_free(&path); + return error; +} + +int git_submodule_save(git_submodule *submodule) +{ + int error = 0; + git_config_file *mods; + git_buf key = GIT_BUF_INIT; + + assert(submodule); + + mods = open_gitmodules(submodule->owner, true, NULL); + if (!mods) { + giterr_set(GITERR_SUBMODULE, + "Adding submodules to a bare repository is not supported (for now)"); + return -1; + } + + if ((error = git_buf_printf(&key, "submodule.%s.", submodule->name)) < 0) + goto cleanup; + + /* save values for path, url, update, ignore, fetchRecurseSubmodules */ + + if ((error = submodule_config_key_trunc_puts(&key, "path")) < 0 || + (error = git_config_file_set_string(mods, key.ptr, submodule->path)) < 0) + goto cleanup; + + if ((error = submodule_config_key_trunc_puts(&key, "url")) < 0 || + (error = git_config_file_set_string(mods, key.ptr, submodule->url)) < 0) + goto cleanup; + + if (!(error = submodule_config_key_trunc_puts(&key, "update")) && + submodule->update != GIT_SUBMODULE_UPDATE_DEFAULT) + { + const char *val = (submodule->update == GIT_SUBMODULE_UPDATE_CHECKOUT) ? + NULL : _sm_update_map[submodule->update].str_match; + error = git_config_file_set_string(mods, key.ptr, val); + } + if (error < 0) + goto cleanup; + + if (!(error = submodule_config_key_trunc_puts(&key, "ignore")) && + submodule->ignore != GIT_SUBMODULE_IGNORE_DEFAULT) + { + const char *val = (submodule->ignore == GIT_SUBMODULE_IGNORE_NONE) ? + NULL : _sm_ignore_map[submodule->ignore].str_match; + error = git_config_file_set_string(mods, key.ptr, val); + } + if (error < 0) + goto cleanup; + + if ((error = submodule_config_key_trunc_puts( + &key, "fetchRecurseSubmodules")) < 0 || + (error = git_config_file_set_string( + mods, key.ptr, submodule->fetch_recurse ? "true" : "false")) < 0) + goto cleanup; + + /* update internal defaults */ + + submodule->ignore_default = submodule->ignore; + submodule->update_default = submodule->update; + submodule->flags |= GIT_SUBMODULE_STATUS_IN_CONFIG; + +cleanup: + if (mods != NULL) + git_config_file_free(mods); + git_buf_free(&key); + + return error; +} + +git_repository *git_submodule_owner(git_submodule *submodule) +{ + assert(submodule); + return submodule->owner; +} + +const char *git_submodule_name(git_submodule *submodule) +{ + assert(submodule); + return submodule->name; +} + +const char *git_submodule_path(git_submodule *submodule) +{ + assert(submodule); + return submodule->path; +} + +const char *git_submodule_url(git_submodule *submodule) +{ + assert(submodule); + return submodule->url; +} + +int git_submodule_set_url(git_submodule *submodule, const char *url) +{ + assert(submodule && url); + + git__free(submodule->url); + + submodule->url = git__strdup(url); + GITERR_CHECK_ALLOC(submodule->url); + + return 0; +} + + const git_oid *git_submodule_index_oid(git_submodule *submodule) +{ + assert(submodule); + + if (submodule->flags & GIT_SUBMODULE_STATUS__INDEX_OID_VALID) + return &submodule->index_oid; + else + return NULL; +} + +const git_oid *git_submodule_head_oid(git_submodule *submodule) +{ + assert(submodule); + + if (submodule->flags & GIT_SUBMODULE_STATUS__HEAD_OID_VALID) + return &submodule->head_oid; + else + return NULL; +} + +const git_oid *git_submodule_wd_oid(git_submodule *submodule) +{ + assert(submodule); + + if (!(submodule->flags & GIT_SUBMODULE_STATUS__WD_OID_VALID)) { + git_repository *subrepo; + + /* calling submodule open grabs the HEAD OID if possible */ + if (!git_submodule_open(&subrepo, submodule)) + git_repository_free(subrepo); + } + + if (submodule->flags & GIT_SUBMODULE_STATUS__WD_OID_VALID) + return &submodule->wd_oid; + else + return NULL; +} + +git_submodule_ignore_t git_submodule_ignore(git_submodule *submodule) +{ + assert(submodule); + return submodule->ignore; +} + +git_submodule_ignore_t git_submodule_set_ignore( + git_submodule *submodule, git_submodule_ignore_t ignore) +{ + git_submodule_ignore_t old; + + assert(submodule); + + if (ignore == GIT_SUBMODULE_IGNORE_DEFAULT) + ignore = submodule->ignore_default; + + old = submodule->ignore; + submodule->ignore = ignore; + return old; +} + +git_submodule_update_t git_submodule_update(git_submodule *submodule) +{ + assert(submodule); + return submodule->update; +} + +git_submodule_update_t git_submodule_set_update( + git_submodule *submodule, git_submodule_update_t update) +{ + git_submodule_update_t old; + + assert(submodule); + + if (update == GIT_SUBMODULE_UPDATE_DEFAULT) + update = submodule->update_default; + + old = submodule->update; + submodule->update = update; + return old; +} + +int git_submodule_init(git_submodule *submodule, int overwrite) +{ + int error; + + /* write "submodule.NAME.url" */ + + if (!submodule->url) { + giterr_set(GITERR_SUBMODULE, + "No URL configured for submodule '%s'", submodule->name); + return -1; + } + + error = submodule_update_config( + submodule, "url", submodule->url, overwrite != 0, false); + if (error < 0) + return error; + + /* write "submodule.NAME.update" if not default */ + + if (submodule->update == GIT_SUBMODULE_UPDATE_CHECKOUT) + error = submodule_update_config( + submodule, "update", NULL, (overwrite != 0), false); + else if (submodule->update != GIT_SUBMODULE_UPDATE_DEFAULT) + error = submodule_update_config( + submodule, "update", + _sm_update_map[submodule->update].str_match, + (overwrite != 0), false); + + return error; +} + +int git_submodule_sync(git_submodule *submodule) +{ + if (!submodule->url) { + giterr_set(GITERR_SUBMODULE, + "No URL configured for submodule '%s'", submodule->name); + return -1; + } + + /* copy URL over to config only if it already exists */ + + return submodule_update_config( + submodule, "url", submodule->url, true, true); +} + +int git_submodule_open( + git_repository **subrepo, + git_submodule *submodule) +{ + int error; + git_buf path = GIT_BUF_INIT; + git_repository *repo; + const char *workdir; + + assert(submodule && subrepo); + + repo = submodule->owner; + workdir = git_repository_workdir(repo); + + if (!workdir) { + giterr_set(GITERR_REPOSITORY, + "Cannot open submodule repository in a bare repo"); + return GIT_ENOTFOUND; + } + + if ((submodule->flags & GIT_SUBMODULE_STATUS_IN_WD) == 0) { + giterr_set(GITERR_REPOSITORY, + "Cannot open submodule repository that is not checked out"); + return GIT_ENOTFOUND; + } + + if (git_buf_joinpath(&path, workdir, submodule->path) < 0) + return -1; + + error = git_repository_open(subrepo, path.ptr); + + git_buf_free(&path); + + /* if we have opened the submodule successfully, let's grab the HEAD OID */ + if (!error && !(submodule->flags & GIT_SUBMODULE_STATUS__WD_OID_VALID)) { + if (!git_reference_name_to_oid( + &submodule->wd_oid, *subrepo, GIT_HEAD_FILE)) + submodule->flags |= GIT_SUBMODULE_STATUS__WD_OID_VALID; + else + giterr_clear(); + } + + return error; +} + +int git_submodule_reload_all(git_repository *repo) +{ + assert(repo); + return load_submodule_config(repo, true); +} + +int git_submodule_reload(git_submodule *submodule) +{ + git_repository *repo; + git_index *index; + int pos, error; + git_tree *head; + git_config_file *mods; + + assert(submodule); + + /* refresh index data */ + + repo = submodule->owner; + if (git_repository_index__weakptr(&index, repo) < 0) + return -1; + + pos = git_index_find(index, submodule->path); + if (pos >= 0) { + git_index_entry *entry = git_index_get(index, pos); + + submodule->flags = submodule->flags & + ~(GIT_SUBMODULE_STATUS_IN_INDEX | + GIT_SUBMODULE_STATUS__INDEX_OID_VALID); + + if ((error = submodule_load_from_index(repo, entry)) < 0) + return error; + } + + /* refresh HEAD tree data */ + + if (!(error = git_repository_head_tree(&head, repo))) { + git_tree_entry *te; + + submodule->flags = submodule->flags & + ~(GIT_SUBMODULE_STATUS_IN_HEAD | + GIT_SUBMODULE_STATUS__HEAD_OID_VALID); + + if (!(error = git_tree_entry_bypath(&te, head, submodule->path))) { + error = submodule_load_from_head(repo, submodule->path, &te->oid); + + git_tree_entry_free(te); + } + else if (error == GIT_ENOTFOUND) { + giterr_clear(); + error = 0; + } + + git_tree_free(head); + } + + if (error < 0) + return error; + + /* refresh config data */ + + if ((mods = open_gitmodules(repo, false, NULL)) != NULL) { + git_buf path = GIT_BUF_INIT; + + git_buf_sets(&path, "submodule\\."); + git_buf_puts_escape_regex(&path, submodule->name); + git_buf_puts(&path, ".*"); + + if (git_buf_oom(&path)) + error = -1; + else + error = git_config_file_foreach_match( + mods, path.ptr, submodule_load_from_config, repo); + + git_buf_free(&path); + } + + return error; +} + +int git_submodule_status( + unsigned int *status, + git_submodule *submodule) +{ + assert(status && submodule); + + /* TODO: move status code from below and update */ + + *status = 0; + + return 0; +} + +/* + * INTERNAL FUNCTIONS + */ + +static git_submodule *submodule_alloc(git_repository *repo, const char *name) { git_submodule *sm = git__calloc(1, sizeof(git_submodule)); if (sm == NULL) @@ -69,6 +791,8 @@ static git_submodule *submodule_alloc(const char *name) return NULL; } + sm->owner = repo; + return sm; } @@ -80,69 +804,113 @@ static void submodule_release(git_submodule *sm, int decr) sm->refcount -= decr; if (sm->refcount == 0) { - if (sm->name != sm->path) + if (sm->name != sm->path) { git__free(sm->path); + sm->path = NULL; + } + git__free(sm->name); + sm->name = NULL; + git__free(sm->url); + sm->url = NULL; + + sm->owner = NULL; + git__free(sm); } } -static int submodule_from_entry( - git_strmap *smcfg, git_index_entry *entry) +static git_submodule *submodule_lookup_or_create( + git_repository *repo, const char *n1, const char *n2) { - git_submodule *sm; - void *old_sm; + git_strmap *smcfg = repo->submodules; khiter_t pos; - int error; + git_submodule *sm; - pos = git_strmap_lookup_index(smcfg, entry->path); + assert(n1); - if (git_strmap_valid_index(smcfg, pos)) - sm = git_strmap_value_at(smcfg, pos); + pos = git_strmap_lookup_index(smcfg, n1); + + if (!git_strmap_valid_index(smcfg, pos) && n2) + pos = git_strmap_lookup_index(smcfg, n2); + + if (!git_strmap_valid_index(smcfg, pos)) + sm = submodule_alloc(repo, n1); else - sm = submodule_alloc(entry->path); + sm = git_strmap_value_at(smcfg, pos); - git_oid_cpy(&sm->oid, &entry->oid); + return sm; +} - if (strcmp(sm->path, entry->path) != 0) { - if (sm->path != sm->name) { - git__free(sm->path); - sm->path = sm->name; - } - sm->path = git__strdup(entry->path); - if (!sm->path) - goto fail; +static int submodule_update_map( + git_repository *repo, git_submodule *sm, const char *key) +{ + void *old_sm; + int error; + + git_strmap_insert2(repo->submodules, key, sm, old_sm, error); + if (error < 0) { + submodule_release(sm, 0); + return -1; } - git_strmap_insert2(smcfg, sm->path, sm, old_sm, error); - if (error < 0) - goto fail; sm->refcount++; - if (old_sm && ((git_submodule *)old_sm) != sm) { - /* TODO: log warning about multiple entrys for same submodule path */ + if (old_sm && ((git_submodule *)old_sm) != sm) submodule_release(old_sm, 1); - } return 0; +} -fail: - submodule_release(sm, 0); - return -1; +static int submodule_load_from_index( + git_repository *repo, const git_index_entry *entry) +{ + git_submodule *sm = submodule_lookup_or_create(repo, entry->path, NULL); + + if (!sm) + return -1; + + if (sm->flags & GIT_SUBMODULE_STATUS_IN_INDEX) { + sm->flags |= GIT_SUBMODULE_STATUS__INDEX_MULTIPLE_ENTRIES; + return 0; + } + + sm->flags |= GIT_SUBMODULE_STATUS_IN_INDEX; + + git_oid_cpy(&sm->index_oid, &entry->oid); + sm->flags |= GIT_SUBMODULE_STATUS__INDEX_OID_VALID; + + return submodule_update_map(repo, sm, sm->path); } -static int submodule_from_config( +static int submodule_load_from_head( + git_repository *repo, const char *path, const git_oid *oid) +{ + git_submodule *sm = submodule_lookup_or_create(repo, path, NULL); + + if (!sm) + return -1; + + sm->flags |= GIT_SUBMODULE_STATUS_IN_HEAD; + + git_oid_cpy(&sm->head_oid, oid); + sm->flags |= GIT_SUBMODULE_STATUS__HEAD_OID_VALID; + + return submodule_update_map(repo, sm, sm->path); +} + +static int submodule_load_from_config( const char *key, const char *value, void *data) { - git_strmap *smcfg = data; + git_repository *repo = data; + git_strmap *smcfg = repo->submodules; const char *namestart; const char *property; git_buf name = GIT_BUF_INIT; git_submodule *sm; void *old_sm = NULL; bool is_path; - khiter_t pos; int error; if (git__prefixcmp(key, "submodule.") != 0) @@ -153,21 +921,17 @@ static int submodule_from_config( if (property == NULL) return 0; property++; - is_path = (strcmp(property, "path") == 0); + is_path = (strcasecmp(property, "path") == 0); if (git_buf_set(&name, namestart, property - namestart - 1) < 0) return -1; - pos = git_strmap_lookup_index(smcfg, name.ptr); - if (!git_strmap_valid_index(smcfg, pos) && is_path) - pos = git_strmap_lookup_index(smcfg, value); - if (!git_strmap_valid_index(smcfg, pos)) - sm = submodule_alloc(name.ptr); - else - sm = git_strmap_value_at(smcfg, pos); + sm = submodule_lookup_or_create(repo, name.ptr, is_path ? value : NULL); if (!sm) goto fail; + sm->flags |= GIT_SUBMODULE_STATUS_IN_CONFIG; + if (strcmp(sm->name, name.ptr) != 0) { assert(sm->path == sm->name); sm->name = git_buf_detach(&name); @@ -177,7 +941,7 @@ static int submodule_from_config( goto fail; sm->refcount++; } - else if (is_path && strcmp(sm->path, value) != 0) { + else if (is_path && value && strcmp(sm->path, value) != 0) { assert(sm->path == sm->name); sm->path = git__strdup(value); if (sm->path == NULL) @@ -195,19 +959,23 @@ static int submodule_from_config( submodule_release(old_sm, 1); } + /* TODO: Look up path in index and if it is present but not a GITLINK + * then this should be deleted (at least to match git's behavior) + */ + if (is_path) return 0; /* copy other properties into submodule entry */ - if (strcmp(property, "url") == 0) { + if (strcasecmp(property, "url") == 0) { if (sm->url) { git__free(sm->url); sm->url = NULL; } - if ((sm->url = git__strdup(value)) == NULL) + if (value != NULL && (sm->url = git__strdup(value)) == NULL) goto fail; } - else if (strcmp(property, "update") == 0) { + else if (strcasecmp(property, "update") == 0) { int val; if (git_config_lookup_map_value( _sm_update_map, ARRAY_SIZE(_sm_update_map), value, &val) < 0) { @@ -215,16 +983,16 @@ static int submodule_from_config( "Invalid value for submodule update property: '%s'", value); goto fail; } - sm->update = (git_submodule_update_t)val; + sm->update_default = sm->update = (git_submodule_update_t)val; } - else if (strcmp(property, "fetchRecurseSubmodules") == 0) { + else if (strcasecmp(property, "fetchRecurseSubmodules") == 0) { if (git__parse_bool(&sm->fetch_recurse, value) < 0) { giterr_set(GITERR_INVALID, "Invalid value for submodule 'fetchRecurseSubmodules' property: '%s'", value); goto fail; } } - else if (strcmp(property, "ignore") == 0) { + else if (strcasecmp(property, "ignore") == 0) { int val; if (git_config_lookup_map_value( _sm_ignore_map, ARRAY_SIZE(_sm_ignore_map), value, &val) < 0) { @@ -232,7 +1000,7 @@ static int submodule_from_config( "Invalid value for submodule ignore property: '%s'", value); goto fail; } - sm->ignore = (git_submodule_ignore_t)val; + sm->ignore_default = sm->ignore = (git_submodule_ignore_t)val; } /* ignore other unknown submodule properties */ @@ -244,144 +1012,471 @@ fail: return -1; } -static int load_submodule_config(git_repository *repo) +static int submodule_load_from_wd_lite( + git_submodule *sm, const char *name, void *payload) +{ + git_repository *repo = git_submodule_owner(sm); + git_buf path = GIT_BUF_INIT; + + GIT_UNUSED(name); + GIT_UNUSED(payload); + + if (git_buf_joinpath(&path, git_repository_workdir(repo), sm->path) < 0) + return -1; + + if (git_path_isdir(path.ptr)) + sm->flags |= GIT_SUBMODULE_STATUS__WD_SCANNED; + + if (git_path_contains(&path, DOT_GIT)) + sm->flags |= GIT_SUBMODULE_STATUS_IN_WD; + + git_buf_free(&path); + + return 0; +} + +static int load_submodule_config_from_index( + git_repository *repo, git_oid *gitmodules_oid) { int error; - git_index *index; - unsigned int i, max_i; - git_oid gitmodules_oid; - git_strmap *smcfg; - struct git_config_file *mods = NULL; + git_iterator *i; + const git_index_entry *entry; - if (repo->submodules) - return 0; + if ((error = git_iterator_for_index(&i, repo)) < 0) + return error; - /* submodule data is kept in a hashtable with each submodule stored - * under both its name and its path. These are usually the same, but - * that is not guaranteed. - */ - smcfg = git_strmap_alloc(); - GITERR_CHECK_ALLOC(smcfg); + error = git_iterator_current(i, &entry); - /* scan index for gitmodules (and .gitmodules entry) */ - if ((error = git_repository_index__weakptr(&index, repo)) < 0) - goto cleanup; - memset(&gitmodules_oid, 0, sizeof(gitmodules_oid)); - max_i = git_index_entrycount(index); + while (!error && entry != NULL) { - for (i = 0; i < max_i; i++) { - git_index_entry *entry = git_index_get(index, i); if (S_ISGITLINK(entry->mode)) { - if ((error = submodule_from_entry(smcfg, entry)) < 0) - goto cleanup; - } - else if (strcmp(entry->path, ".gitmodules") == 0) - git_oid_cpy(&gitmodules_oid, &entry->oid); + error = submodule_load_from_index(repo, entry); + if (error < 0) + break; + } else if (strcmp(entry->path, GIT_MODULES_FILE) == 0) + git_oid_cpy(gitmodules_oid, &entry->oid); + + error = git_iterator_advance(i, &entry); } - /* load .gitmodules from workdir if it exists */ - if (git_repository_workdir(repo) != NULL) { - /* look in workdir for .gitmodules */ - git_buf path = GIT_BUF_INIT; - if (!git_buf_joinpath( - &path, git_repository_workdir(repo), ".gitmodules") && - git_path_isfile(path.ptr)) - { - if (!(error = git_config_file__ondisk(&mods, path.ptr))) - error = git_config_file_open(mods); + git_iterator_free(i); + + return error; +} + +static int load_submodule_config_from_head( + git_repository *repo, git_oid *gitmodules_oid) +{ + int error; + git_tree *head; + git_iterator *i; + const git_index_entry *entry; + + if ((error = git_repository_head_tree(&head, repo)) < 0) + return error; + + if ((error = git_iterator_for_tree(&i, repo, head)) < 0) { + git_tree_free(head); + return error; + } + + error = git_iterator_current(i, &entry); + + while (!error && entry != NULL) { + + if (S_ISGITLINK(entry->mode)) { + error = submodule_load_from_head(repo, entry->path, &entry->oid); + if (error < 0) + break; + } else if (strcmp(entry->path, GIT_MODULES_FILE) == 0 && + git_oid_iszero(gitmodules_oid)) + git_oid_cpy(gitmodules_oid, &entry->oid); + + error = git_iterator_advance(i, &entry); + } + + git_iterator_free(i); + git_tree_free(head); + + return error; +} + +static git_config_file *open_gitmodules( + git_repository *repo, + bool okay_to_create, + const git_oid *gitmodules_oid) +{ + const char *workdir = git_repository_workdir(repo); + git_buf path = GIT_BUF_INIT; + git_config_file *mods = NULL; + + if (workdir != NULL) { + if (git_buf_joinpath(&path, workdir, GIT_MODULES_FILE) != 0) + return NULL; + + if (okay_to_create || git_path_isfile(path.ptr)) { + /* git_config_file__ondisk should only fail if OOM */ + if (git_config_file__ondisk(&mods, path.ptr) < 0) + return NULL; + + /* open should only fail here if the file is malformed */ + if (git_config_file_open(mods) < 0) { + git_config_file_free(mods); + mods = NULL; + } } - git_buf_free(&path); } - /* load .gitmodules from object cache if not in workdir */ - if (!error && mods == NULL && !git_oid_iszero(&gitmodules_oid)) { - /* TODO: is it worth loading gitmodules from object cache? */ + if (!mods && gitmodules_oid && !git_oid_iszero(gitmodules_oid)) { + /* TODO: Retrieve .gitmodules content from ODB */ + + /* Should we actually do this? Core git does not, but it means you + * can't really get much information about submodules on bare repos. + */ + } + + return mods; +} + +static int load_submodule_config(git_repository *repo, bool force) +{ + int error; + git_oid gitmodules_oid; + git_buf path = GIT_BUF_INIT; + git_config_file *mods = NULL; + + if (repo->submodules && !force) + return 0; + + memset(&gitmodules_oid, 0, sizeof(gitmodules_oid)); + + /* Submodule data is kept in a hashtable keyed by both name and path. + * These are usually the same, but that is not guaranteed. + */ + if (!repo->submodules) { + repo->submodules = git_strmap_alloc(); + GITERR_CHECK_ALLOC(repo->submodules); } - /* process .gitmodules info */ - if (!error && mods != NULL) - error = git_config_file_foreach(mods, submodule_from_config, smcfg); + /* add submodule information from index */ + + if ((error = load_submodule_config_from_index(repo, &gitmodules_oid)) < 0) + goto cleanup; - /* store submodule config in repo */ - if (!error) - repo->submodules = smcfg; + /* add submodule information from HEAD */ + + if ((error = load_submodule_config_from_head(repo, &gitmodules_oid)) < 0) + goto cleanup; + + /* add submodule information from .gitmodules */ + + if ((mods = open_gitmodules(repo, false, &gitmodules_oid)) != NULL) + error = git_config_file_foreach(mods, submodule_load_from_config, repo); + + if (error != 0) + goto cleanup; + + /* shallow scan submodules in work tree */ + + if (!git_repository_is_bare(repo)) + error = git_submodule_foreach(repo, submodule_load_from_wd_lite, NULL); cleanup: + git_buf_free(&path); + if (mods != NULL) git_config_file_free(mods); + if (error) - git_strmap_free(smcfg); + git_submodule_config_free(repo); + return error; } -void git_submodule_config_free(git_repository *repo) +static int lookup_head_remote(git_buf *url, git_repository *repo) { - git_strmap *smcfg = repo->submodules; - git_submodule *sm; + int error; + git_config *cfg; + git_reference *head = NULL, *remote = NULL; + const char *tgt, *scan; + git_buf key = GIT_BUF_INIT; + + /* 1. resolve HEAD -> refs/heads/BRANCH + * 2. lookup config branch.BRANCH.remote -> ORIGIN + * 3. lookup remote.ORIGIN.url + */ - repo->submodules = NULL; + if ((error = git_repository_config__weakptr(&cfg, repo)) < 0) + return error; - if (smcfg == NULL) - return; + if (git_reference_lookup(&head, repo, GIT_HEAD_FILE) < 0) { + giterr_set(GITERR_SUBMODULE, + "Cannot resolve relative URL when HEAD cannot be resolved"); + error = GIT_ENOTFOUND; + goto cleanup; + } - git_strmap_foreach_value(smcfg, sm, { - submodule_release(sm,1); - }); - git_strmap_free(smcfg); + if (git_reference_type(head) != GIT_REF_SYMBOLIC) { + giterr_set(GITERR_SUBMODULE, + "Cannot resolve relative URL when HEAD is not symbolic"); + error = GIT_ENOTFOUND; + goto cleanup; + } + + if ((error = git_branch_tracking(&remote, head)) < 0) + goto cleanup; + + /* remote should refer to something like refs/remotes/ORIGIN/BRANCH */ + + if (git_reference_type(remote) != GIT_REF_SYMBOLIC || + git__prefixcmp(git_reference_target(remote), "refs/remotes/") != 0) + { + giterr_set(GITERR_SUBMODULE, + "Cannot resolve relative URL when HEAD is not symbolic"); + error = GIT_ENOTFOUND; + goto cleanup; + } + + scan = tgt = git_reference_target(remote) + strlen("refs/remotes/"); + while (*scan && (*scan != '/' || (scan > tgt && scan[-1] != '\\'))) + scan++; /* find non-escaped slash to end ORIGIN name */ + + error = git_buf_printf(&key, "remote.%.*s.url", (int)(scan - tgt), tgt); + if (error < 0) + goto cleanup; + + if ((error = git_config_get_string(&tgt, cfg, key.ptr)) < 0) + goto cleanup; + + error = git_buf_sets(url, tgt); + +cleanup: + git_buf_free(&key); + git_reference_free(head); + git_reference_free(remote); + + return error; } -static int submodule_cmp(const void *a, const void *b) +static int submodule_update_config( + git_submodule *submodule, + const char *attr, + const char *value, + bool overwrite, + bool only_existing) { - return strcmp(((git_submodule *)a)->name, ((git_submodule *)b)->name); + int error; + git_config *config; + git_buf key = GIT_BUF_INIT; + const char *old = NULL; + + assert(submodule); + + error = git_repository_config__weakptr(&config, submodule->owner); + if (error < 0) + return error; + + error = git_buf_printf(&key, "submodule.%s.%s", submodule->name, attr); + if (error < 0) + goto cleanup; + + if (git_config_get_string(&old, config, key.ptr) < 0) + giterr_clear(); + + if (!old && only_existing) + goto cleanup; + if (old && !overwrite) + goto cleanup; + if ((!old && !value) || (old && value && strcmp(old, value) == 0)) + goto cleanup; + + if (!value) + error = git_config_delete(config, key.ptr); + else + error = git_config_set_string(config, key.ptr, value); + +cleanup: + git_buf_free(&key); + return error; } -int git_submodule_foreach( - git_repository *repo, - int (*callback)(const char *name, void *payload), - void *payload) +#if 0 + +static int head_oid_for_submodule( + git_oid *oid, + git_repository *owner, + const char *path) +{ + int error = 0; + git_oid head_oid; + git_tree *head_tree = NULL, *container_tree = NULL; + unsigned int pos; + const git_tree_entry *entry; + + if (git_reference_name_to_oid(&head_oid, owner, GIT_HEAD_FILE) < 0 || + git_tree_lookup(&head_tree, owner, &head_oid) < 0 || + git_tree_resolve_path(&container_tree, &pos, head_tree, path) < 0 || + (entry = git_tree_entry_byindex(container_tree, pos)) == NULL) + { + memset(oid, 0, sizeof(*oid)); + error = GIT_ENOTFOUND; + } + else { + git_oid_cpy(oid, &entry->oid); + } + + git_tree_free(head_tree); + git_tree_free(container_tree); + + return error; +} + +int git_submodule_status( + unsigned int *status, + git_oid *head, + git_submodule *sm, + git_submodule_ignore_t ignore) { int error; - git_submodule *sm; - git_vector seen = GIT_VECTOR_INIT; - seen._cmp = submodule_cmp; + const char *workdir; + git_repository *owner, *sm_repo = NULL; + git_oid owner_head, sm_head; - if ((error = load_submodule_config(repo)) < 0) - return error; + assert(submodule && status); - git_strmap_foreach_value(repo->submodules, sm, { - /* usually the following will not come into play */ - if (sm->refcount > 1) { - if (git_vector_bsearch(&seen, sm) != GIT_ENOTFOUND) - continue; - if ((error = git_vector_insert(&seen, sm)) < 0) - break; + if (head == NULL) + head = &sm_head; + + owner = submodule->owner; + workdir = git_repository_workdir(owner); + + if (ignore == GIT_SUBMODULE_IGNORE_DEFAULT) + ignore = sm->ignore; + + /* if this is a bare repo or the submodule dir has no .git yet, + * then it is not checked out and we'll just return index data. + */ + if (!workdir || (sm->flags & GIT_SUBMODULE_FLAG__HAS_DOTGIT) == 0) { + *status = GIT_SUBMODULE_STATUS_NOT_CHECKED_OUT; + + if (sm->index_oid_valid) + git_oid_cpy(head, &sm->index_oid); + else + memset(head, 0, sizeof(git_oid)); + + if (git_oid_iszero(head)) { + if (sm->url) + *status = GIT_SUBMODULE_STATUS_NEW_SUBMODULE; + } else if (!sm->url) { + *status = GIT_SUBMODULE_STATUS_DELETED_SUBMODULE; } - if ((error = callback(sm->name, payload)) < 0) - break; - }); + return 0; + } - git_vector_free(&seen); + /* look up submodule path in repo head to find if new or deleted */ + if ((error = head_oid_for_submodule(&owner_head, owner, sm->path)) < 0) { + *status = GIT_SUBMODULE_STATUS_NEW_SUBMODULE; + /* ??? */ + } + + if (ignore == GIT_SUBMODULE_IGNORE_ALL) { + *status = GIT_SUBMODULE_STATUS_CLEAN; + git_oid_cpy(head, &sm->oid); + return 0; + } + + if ((error = git_submodule_open(&sm_repo, sm)) < 0) + return error; + + if ((error = git_reference_name_to_oid(head, sm_repo, GIT_HEAD_FILE)) < 0) + goto cleanup; + + if (ignore == GIT_SUBMODULE_IGNORE_DIRTY && + git_oid_cmp(head, &sm->oid) == 0) + { + *status = GIT_SUBMODULE_STATUS_CLEAN; + return 0; + } + + /* look up submodule oid from index in repo to find if new commits or missing commits */ + + /* run a short status to find if modified or untracked content */ + +#define GIT_SUBMODULE_STATUS_NEW_SUBMODULE (1u << 2) +#define GIT_SUBMODULE_STATUS_DELETED_SUBMODULE (1u << 3) +#define GIT_SUBMODULE_STATUS_NOT_CHECKED_OUT (1u << 4) +#define GIT_SUBMODULE_STATUS_NEW_COMMITS (1u << 5) +#define GIT_SUBMODULE_STATUS_MISSING_COMMITS (1u << 6) +#define GIT_SUBMODULE_STATUS_MODIFIED_CONTENT (1u << 7) +#define GIT_SUBMODULE_STATUS_UNTRACKED_CONTENT (1u << 8) + +cleanup: + git_repository_free(sm_repo); + git_tree_free(owner_tree); return error; } -int git_submodule_lookup( - git_submodule **sm_ptr, /* NULL allowed if user only wants to test */ +int git_submodule_status_for_path( + unsigned int *status, + git_oid *head, git_repository *repo, - const char *name) /* trailing slash is allowed */ + const char *submodule_path, + git_submodule_ignore_t ignore) { - khiter_t pos; + int error; + git_submodule *sm; + const char *workdir; + git_buf path = GIT_BUF_INIT; + git_oid owner_head; + + assert(repo && submodule_path && status); + + if ((error = git_submodule_lookup(&sm, repo, submodule_path)) == 0) + return git_submodule_status(status, head, sm, ignore); - if (load_submodule_config(repo) < 0) + /* if submodule still exists in HEAD, then it is DELETED */ + if (!(error = head_oid_for_submodule(&owner_head, repo, submodule_path))) { + *status = GIT_SUBMODULE_STATUS_DELETED_SUBMODULE; + if (head) + git_oid_cmp(head, &owner_head); + return 0; + } + + /* submodule was not found - let's see what we can determine about it */ + workdir = git_repository_workdir(repo); + + if (error != GIT_ENOTFOUND || !workdir) { + *status = GIT_SUBMODULE_STATUS_NOT_A_SUBMODULE; + return error; + } + + giterr_clear(); + error = 0; + + /* figure out if this is NEW, NOT_CHECKED_OUT, or what */ + if (git_buf_joinpath(&path, workdir, submodule_path) < 0) return -1; - pos = git_strmap_lookup_index(repo->submodules, name); - if (!git_strmap_valid_index(repo->submodules, pos)) - return GIT_ENOTFOUND; + if (git_path_contains(&path, DOT_GIT)) { + git_repository *sm_repo; - if (sm_ptr) - *sm_ptr = git_strmap_value_at(repo->submodules, pos); + *status = GIT_SUBMODULE_STATUS_UNTRACKED_SUBMODULE; - return 0; + /* only bother look up head if it was non-NULL */ + if (head != NULL && + !(error = git_repository_open(&sm_repo, path.ptr))) + { + error = git_reference_name_to_oid(head, sm_repo, GIT_HEAD_FILE); + git_repository_free(sm_repo); + } + } else + *status = GIT_SUBMODULE_STATUS_NOT_A_SUBMODULE; + + git_buf_free(&path); + + return error; } + +#endif diff --git a/src/submodule.h b/src/submodule.h new file mode 100644 index 000000000..83bc7dfe9 --- /dev/null +++ b/src/submodule.h @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2012 the libgit2 contributors + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_submodule_h__ +#define INCLUDE_submodule_h__ + +/* Notes: + * + * Submodule information can be in four places: the index, the config files + * (both .git/config and .gitmodules), the HEAD tree, and the working + * directory. + * + * In the index: + * - submodule is found by path + * - may be missing, present, or of the wrong type + * - will have an oid if present + * + * In the HEAD tree: + * - submodule is found by path + * - may be missing, present, or of the wrong type + * - will have an oid if present + * + * In the config files: + * - submodule is found by submodule "name" which is usually the path + * - may be missing or present + * - will have a name, path, url, and other properties + * + * In the working directory: + * - submodule is found by path + * - may be missing, an empty directory, a checked out directory, + * or of the wrong type + * - if checked out, will have a HEAD oid + * - if checked out, will have git history that can be used to compare oids + * - if checked out, may have modified files and/or untracked files + */ + +/** + * Description of submodule + * + * This record describes a submodule found in a repository. There should be + * an entry for every submodule found in the HEAD and index, and for every + * submodule described in .gitmodules. The fields are as follows: + * + * - `owner` is the git_repository containing this submodule + * - `name` is the name of the submodule from .gitmodules. + * - `path` is the path to the submodule from the repo root. It is almost + * always the same as `name`. + * - `url` is the url for the submodule. + * - `tree_oid` is the SHA1 for the submodule path in the repo HEAD. + * - `index_oid` is the SHA1 for the submodule recorded in the index. + * - `workdir_oid` is the SHA1 for the HEAD of the checked out submodule. + * - `update` is a git_submodule_update_t value - see gitmodules(5) update. + * - `ignore` is a git_submodule_ignore_t value - see gitmodules(5) ignore. + * - `fetch_recurse` is 0 or 1 - see gitmodules(5) fetchRecurseSubmodules. + * - `refcount` tracks how many hashmap entries there are for this submodule. + * It only comes into play if the name and path of the submodule differ. + * - `flags` is for internal use, tracking where this submodule has been + * found (head, index, config, workdir) and other misc info about it. + * + * If the submodule has been added to .gitmodules but not yet git added, + * then the `index_oid` will be valid and zero. If the submodule has been + * deleted, but the delete has not been committed yet, then the `index_oid` + * will be set, but the `url` will be NULL. + */ +struct git_submodule { + git_repository *owner; + char *name; + char *path; /* important: may point to same string data as "name" */ + char *url; + uint32_t flags; + git_oid head_oid; + git_oid index_oid; + git_oid wd_oid; + /* information from config */ + git_submodule_update_t update; + git_submodule_update_t update_default; + git_submodule_ignore_t ignore; + git_submodule_ignore_t ignore_default; + int fetch_recurse; + /* internal information */ + int refcount; +}; + +/* Additional flags on top of public GIT_SUBMODULE_STATUS values */ +#define GIT_SUBMODULE_STATUS__WD_SCANNED (1u << 15) +#define GIT_SUBMODULE_STATUS__HEAD_OID_VALID (1u << 16) +#define GIT_SUBMODULE_STATUS__INDEX_OID_VALID (1u << 17) +#define GIT_SUBMODULE_STATUS__WD_OID_VALID (1u << 18) +#define GIT_SUBMODULE_STATUS__INDEX_MULTIPLE_ENTRIES (1u << 19) + +#endif diff --git a/tests-clar/status/submodules.c b/tests-clar/status/submodules.c index 9423e8490..3a69e0c47 100644 --- a/tests-clar/status/submodules.c +++ b/tests-clar/status/submodules.c @@ -3,24 +3,17 @@ #include "path.h" #include "posix.h" #include "status_helpers.h" +#include "../submodule/submodule_helpers.h" static git_repository *g_repo = NULL; void test_status_submodules__initialize(void) { - git_buf modpath = GIT_BUF_INIT; - g_repo = cl_git_sandbox_init("submodules"); cl_fixture_sandbox("testrepo.git"); - cl_git_pass(git_buf_sets(&modpath, git_repository_workdir(g_repo))); - cl_assert(git_path_dirname_r(&modpath, modpath.ptr) >= 0); - cl_git_pass(git_buf_joinpath(&modpath, modpath.ptr, "testrepo.git\n")); - - p_rename("submodules/gitmodules", "submodules/.gitmodules"); - cl_git_append2file("submodules/.gitmodules", modpath.ptr); - git_buf_free(&modpath); + rewrite_gitmodules(git_repository_workdir(g_repo)); p_rename("submodules/testrepo/.gitted", "submodules/testrepo/.git"); } @@ -28,6 +21,7 @@ void test_status_submodules__initialize(void) void test_status_submodules__cleanup(void) { cl_git_sandbox_cleanup(); + cl_fixture_cleanup("testrepo.git"); } void test_status_submodules__api(void) @@ -40,8 +34,8 @@ void test_status_submodules__api(void) cl_git_pass(git_submodule_lookup(&sm, g_repo, "testrepo")); cl_assert(sm != NULL); - cl_assert_equal_s("testrepo", sm->name); - cl_assert_equal_s("testrepo", sm->path); + cl_assert_equal_s("testrepo", git_submodule_name(sm)); + cl_assert_equal_s("testrepo", git_submodule_path(sm)); } void test_status_submodules__0(void) diff --git a/tests-clar/submodule/lookup.c b/tests-clar/submodule/lookup.c new file mode 100644 index 000000000..669338f1c --- /dev/null +++ b/tests-clar/submodule/lookup.c @@ -0,0 +1,110 @@ +#include "clar_libgit2.h" +#include "submodule_helpers.h" +#include "posix.h" + +static git_repository *g_repo = NULL; + +void test_submodule_lookup__initialize(void) +{ + g_repo = cl_git_sandbox_init("submod2"); + + cl_fixture_sandbox("submod2_target"); + p_rename("submod2_target/.gitted", "submod2_target/.git"); + + /* must create submod2_target before rewrite so prettify will work */ + rewrite_gitmodules(git_repository_workdir(g_repo)); + p_rename("submod2/not_submodule/.gitted", "submod2/not_submodule/.git"); +} + +void test_submodule_lookup__cleanup(void) +{ + cl_git_sandbox_cleanup(); + cl_fixture_cleanup("submod2_target"); +} + +void test_submodule_lookup__simple_lookup(void) +{ + git_submodule *sm; + + /* lookup existing */ + cl_git_pass(git_submodule_lookup(&sm, g_repo, "sm_unchanged")); + cl_assert(sm); + + /* lookup pending change in .gitmodules that is not in HEAD */ + cl_git_pass(git_submodule_lookup(&sm, g_repo, "sm_added_and_uncommited")); + cl_assert(sm); + + /* lookup git repo subdir that is not added as submodule */ + cl_assert(git_submodule_lookup(&sm, g_repo, "not_submodule") == GIT_EEXISTS); + + /* lookup existing directory that is not a submodule */ + cl_assert(git_submodule_lookup(&sm, g_repo, "just_a_dir") == GIT_ENOTFOUND); + + /* lookup existing file that is not a submodule */ + cl_assert(git_submodule_lookup(&sm, g_repo, "just_a_file") == GIT_ENOTFOUND); + + /* lookup non-existent item */ + cl_assert(git_submodule_lookup(&sm, g_repo, "no_such_file") == GIT_ENOTFOUND); +} + +void test_submodule_lookup__accessors(void) +{ + git_submodule *sm; + const char *oid = "480095882d281ed676fe5b863569520e54a7d5c0"; + + cl_git_pass(git_submodule_lookup(&sm, g_repo, "sm_unchanged")); + cl_assert(git_submodule_owner(sm) == g_repo); + cl_assert_equal_s("sm_unchanged", git_submodule_name(sm)); + cl_assert(git__suffixcmp(git_submodule_path(sm), "sm_unchanged") == 0); + cl_assert(git__suffixcmp(git_submodule_url(sm), "/submod2_target") == 0); + + cl_assert(git_oid_streq(git_submodule_index_oid(sm), oid) == 0); + cl_assert(git_oid_streq(git_submodule_head_oid(sm), oid) == 0); + cl_assert(git_oid_streq(git_submodule_wd_oid(sm), oid) == 0); + + cl_assert(git_submodule_ignore(sm) == GIT_SUBMODULE_IGNORE_NONE); + cl_assert(git_submodule_update(sm) == GIT_SUBMODULE_UPDATE_CHECKOUT); + + cl_git_pass(git_submodule_lookup(&sm, g_repo, "sm_changed_head")); + cl_assert_equal_s("sm_changed_head", git_submodule_name(sm)); + + cl_assert(git_oid_streq(git_submodule_index_oid(sm), oid) == 0); + cl_assert(git_oid_streq(git_submodule_head_oid(sm), oid) == 0); + cl_assert(git_oid_streq(git_submodule_wd_oid(sm), + "3d9386c507f6b093471a3e324085657a3c2b4247") == 0); + + cl_git_pass(git_submodule_lookup(&sm, g_repo, "sm_added_and_uncommited")); + cl_assert_equal_s("sm_added_and_uncommited", git_submodule_name(sm)); + + cl_assert(git_oid_streq(git_submodule_index_oid(sm), oid) == 0); + cl_assert(git_submodule_head_oid(sm) == NULL); + cl_assert(git_oid_streq(git_submodule_wd_oid(sm), oid) == 0); + + cl_git_pass(git_submodule_lookup(&sm, g_repo, "sm_missing_commits")); + cl_assert_equal_s("sm_missing_commits", git_submodule_name(sm)); + + cl_assert(git_oid_streq(git_submodule_index_oid(sm), oid) == 0); + cl_assert(git_oid_streq(git_submodule_head_oid(sm), oid) == 0); + cl_assert(git_oid_streq(git_submodule_wd_oid(sm), + "5e4963595a9774b90524d35a807169049de8ccad") == 0); +} + +typedef struct { + int count; +} sm_lookup_data; + +static int sm_lookup_cb(git_submodule *sm, const char *name, void *payload) +{ + sm_lookup_data *data = payload; + data->count += 1; + cl_assert_equal_s(git_submodule_name(sm), name); + return 0; +} + +void test_submodule_lookup__foreach(void) +{ + sm_lookup_data data; + memset(&data, 0, sizeof(data)); + cl_git_pass(git_submodule_foreach(g_repo, sm_lookup_cb, &data)); + cl_assert_equal_i(7, data.count); +} diff --git a/tests-clar/submodule/modify.c b/tests-clar/submodule/modify.c new file mode 100644 index 000000000..7f04ce0f5 --- /dev/null +++ b/tests-clar/submodule/modify.c @@ -0,0 +1,256 @@ +#include "clar_libgit2.h" +#include "posix.h" +#include "path.h" +#include "submodule_helpers.h" + +static git_repository *g_repo = NULL; + +#define SM_LIBGIT2_URL "https://github.com/libgit2/libgit2.git" +#define SM_LIBGIT2 "sm_libgit2" +#define SM_LIBGIT2B "sm_libgit2b" + +void test_submodule_modify__initialize(void) +{ + g_repo = cl_git_sandbox_init("submod2"); + + cl_fixture_sandbox("submod2_target"); + p_rename("submod2_target/.gitted", "submod2_target/.git"); + + /* must create submod2_target before rewrite so prettify will work */ + rewrite_gitmodules(git_repository_workdir(g_repo)); + p_rename("submod2/not_submodule/.gitted", "submod2/not_submodule/.git"); +} + +void test_submodule_modify__cleanup(void) +{ + cl_git_sandbox_cleanup(); + cl_fixture_cleanup("submod2_target"); +} + +void test_submodule_modify__add(void) +{ + git_submodule *sm; + git_config *cfg; + const char *s; + + /* re-add existing submodule */ + cl_assert( + git_submodule_add_setup(NULL, g_repo, "whatever", "sm_unchanged", 1) == + GIT_EEXISTS ); + + /* add a submodule using a gitlink */ + + cl_git_pass( + git_submodule_add_setup(&sm, g_repo, SM_LIBGIT2_URL, SM_LIBGIT2, 1) + ); + + cl_assert(git_path_isfile("submod2/" SM_LIBGIT2 "/.git")); + + cl_assert(git_path_isdir("submod2/.git/modules")); + cl_assert(git_path_isdir("submod2/.git/modules/" SM_LIBGIT2)); + cl_assert(git_path_isfile("submod2/.git/modules/" SM_LIBGIT2 "/HEAD")); + + cl_git_pass(git_repository_config(&cfg, g_repo)); + cl_git_pass( + git_config_get_string(&s, cfg, "submodule." SM_LIBGIT2 ".url")); + cl_assert_equal_s(s, SM_LIBGIT2_URL); + git_config_free(cfg); + + /* add a submodule not using a gitlink */ + + cl_git_pass( + git_submodule_add_setup(&sm, g_repo, SM_LIBGIT2_URL, SM_LIBGIT2B, 0) + ); + + cl_assert(git_path_isdir("submod2/" SM_LIBGIT2B "/.git")); + cl_assert(git_path_isfile("submod2/" SM_LIBGIT2B "/.git/HEAD")); + cl_assert(!git_path_exists("submod2/.git/modules/" SM_LIBGIT2B)); + + cl_git_pass(git_repository_config(&cfg, g_repo)); + cl_git_pass( + git_config_get_string(&s, cfg, "submodule." SM_LIBGIT2B ".url")); + cl_assert_equal_s(s, SM_LIBGIT2_URL); + git_config_free(cfg); +} + +static int delete_one_config( + const char *var_name, const char *value, void *payload) +{ + git_config *cfg = payload; + GIT_UNUSED(value); + return git_config_delete(cfg, var_name); +} + +static int init_one_submodule( + git_submodule *sm, const char *name, void *payload) +{ + GIT_UNUSED(name); + GIT_UNUSED(payload); + return git_submodule_init(sm, false); +} + +void test_submodule_modify__init(void) +{ + git_config *cfg; + const char *str; + + /* erase submodule data from .git/config */ + cl_git_pass(git_repository_config(&cfg, g_repo)); + cl_git_pass( + git_config_foreach_match(cfg, "submodule\\..*", delete_one_config, cfg)); + git_config_free(cfg); + + /* confirm no submodule data in config */ + cl_git_pass(git_repository_config(&cfg, g_repo)); + cl_git_fail(git_config_get_string(&str, cfg, "submodule.sm_unchanged.url")); + cl_git_fail(git_config_get_string(&str, cfg, "submodule.sm_changed_head.url")); + cl_git_fail(git_config_get_string(&str, cfg, "submodule.sm_added_and_uncommited.url")); + git_config_free(cfg); + + /* call init and see that settings are copied */ + cl_git_pass(git_submodule_foreach(g_repo, init_one_submodule, NULL)); + + git_submodule_reload_all(g_repo); + + /* confirm submodule data in config */ + cl_git_pass(git_repository_config(&cfg, g_repo)); + cl_git_pass(git_config_get_string(&str, cfg, "submodule.sm_unchanged.url")); + cl_assert(git__suffixcmp(str, "/submod2_target") == 0); + cl_git_pass(git_config_get_string(&str, cfg, "submodule.sm_changed_head.url")); + cl_assert(git__suffixcmp(str, "/submod2_target") == 0); + cl_git_pass(git_config_get_string(&str, cfg, "submodule.sm_added_and_uncommited.url")); + cl_assert(git__suffixcmp(str, "/submod2_target") == 0); + git_config_free(cfg); +} + +static int sync_one_submodule( + git_submodule *sm, const char *name, void *payload) +{ + GIT_UNUSED(name); + GIT_UNUSED(payload); + return git_submodule_sync(sm); +} + +void test_submodule_modify__sync(void) +{ + git_submodule *sm1, *sm2, *sm3; + git_config *cfg; + const char *str; + +#define SM1 "sm_unchanged" +#define SM2 "sm_changed_head" +#define SM3 "sm_added_and_uncommited" + + /* look up some submodules */ + cl_git_pass(git_submodule_lookup(&sm1, g_repo, SM1)); + cl_git_pass(git_submodule_lookup(&sm2, g_repo, SM2)); + cl_git_pass(git_submodule_lookup(&sm3, g_repo, SM3)); + + /* At this point, the .git/config URLs for the submodules have + * not be rewritten with the absolute paths (although the + * .gitmodules have. Let's confirm that they DO NOT match + * yet, then we can do a sync to make them match... + */ + + /* check submodule info does not match before sync */ + cl_git_pass(git_repository_config(&cfg, g_repo)); + cl_git_pass(git_config_get_string(&str, cfg, "submodule."SM1".url")); + cl_assert(strcmp(git_submodule_url(sm1), str) != 0); + cl_git_pass(git_config_get_string(&str, cfg, "submodule."SM2".url")); + cl_assert(strcmp(git_submodule_url(sm2), str) != 0); + cl_git_pass(git_config_get_string(&str, cfg, "submodule."SM3".url")); + cl_assert(strcmp(git_submodule_url(sm3), str) != 0); + git_config_free(cfg); + + /* sync all the submodules */ + cl_git_pass(git_submodule_foreach(g_repo, sync_one_submodule, NULL)); + + /* check that submodule config is updated */ + cl_git_pass(git_repository_config(&cfg, g_repo)); + cl_git_pass(git_config_get_string(&str, cfg, "submodule."SM1".url")); + cl_assert_equal_s(git_submodule_url(sm1), str); + cl_git_pass(git_config_get_string(&str, cfg, "submodule."SM2".url")); + cl_assert_equal_s(git_submodule_url(sm2), str); + cl_git_pass(git_config_get_string(&str, cfg, "submodule."SM3".url")); + cl_assert_equal_s(git_submodule_url(sm3), str); + git_config_free(cfg); +} + +void test_submodule_modify__edit_and_save(void) +{ + git_submodule *sm1, *sm2; + char *old_url; + git_submodule_ignore_t old_ignore; + git_submodule_update_t old_update; + git_repository *r2; + + cl_git_pass(git_submodule_lookup(&sm1, g_repo, "sm_changed_head")); + + old_url = git__strdup(git_submodule_url(sm1)); + + /* modify properties of submodule */ + cl_git_pass(git_submodule_set_url(sm1, SM_LIBGIT2_URL)); + old_ignore = git_submodule_set_ignore(sm1, GIT_SUBMODULE_IGNORE_UNTRACKED); + old_update = git_submodule_set_update(sm1, GIT_SUBMODULE_UPDATE_REBASE); + + cl_assert_equal_s(SM_LIBGIT2_URL, git_submodule_url(sm1)); + cl_assert_equal_i( + (int)GIT_SUBMODULE_IGNORE_UNTRACKED, (int)git_submodule_ignore(sm1)); + cl_assert_equal_i( + (int)GIT_SUBMODULE_UPDATE_REBASE, (int)git_submodule_update(sm1)); + + /* revert without saving (and confirm setters return old value) */ + cl_git_pass(git_submodule_set_url(sm1, old_url)); + cl_assert_equal_i( + (int)GIT_SUBMODULE_IGNORE_UNTRACKED, + (int)git_submodule_set_ignore(sm1, GIT_SUBMODULE_IGNORE_DEFAULT)); + cl_assert_equal_i( + (int)GIT_SUBMODULE_UPDATE_REBASE, + (int)git_submodule_set_update(sm1, GIT_SUBMODULE_UPDATE_DEFAULT)); + + /* check that revert was successful */ + cl_assert_equal_s(old_url, git_submodule_url(sm1)); + cl_assert_equal_i((int)old_ignore, (int)git_submodule_ignore(sm1)); + cl_assert_equal_i((int)old_update, (int)git_submodule_update(sm1)); + + /* modify properties of submodule (again) */ + cl_git_pass(git_submodule_set_url(sm1, SM_LIBGIT2_URL)); + git_submodule_set_ignore(sm1, GIT_SUBMODULE_IGNORE_UNTRACKED); + git_submodule_set_update(sm1, GIT_SUBMODULE_UPDATE_REBASE); + + /* call save */ + cl_git_pass(git_submodule_save(sm1)); + + /* attempt to "revert" values */ + git_submodule_set_ignore(sm1, GIT_SUBMODULE_IGNORE_DEFAULT); + git_submodule_set_update(sm1, GIT_SUBMODULE_UPDATE_DEFAULT); + + /* but ignore and update should NOT revert because the DEFAULT + * should now be the newly saved value... + */ + cl_assert_equal_i( + (int)GIT_SUBMODULE_IGNORE_UNTRACKED, (int)git_submodule_ignore(sm1)); + cl_assert_equal_i( + (int)GIT_SUBMODULE_UPDATE_REBASE, (int)git_submodule_update(sm1)); + + /* call reload and check that the new values are loaded */ + cl_git_pass(git_submodule_reload(sm1)); + + cl_assert_equal_s(SM_LIBGIT2_URL, git_submodule_url(sm1)); + cl_assert_equal_i( + (int)GIT_SUBMODULE_IGNORE_UNTRACKED, (int)git_submodule_ignore(sm1)); + cl_assert_equal_i( + (int)GIT_SUBMODULE_UPDATE_REBASE, (int)git_submodule_update(sm1)); + + /* open a second copy of the repo and compare submodule */ + cl_git_pass(git_repository_open(&r2, "submod2")); + cl_git_pass(git_submodule_lookup(&sm2, r2, "sm_changed_head")); + + cl_assert_equal_s(SM_LIBGIT2_URL, git_submodule_url(sm2)); + cl_assert_equal_i( + (int)GIT_SUBMODULE_IGNORE_UNTRACKED, (int)git_submodule_ignore(sm2)); + cl_assert_equal_i( + (int)GIT_SUBMODULE_UPDATE_REBASE, (int)git_submodule_update(sm2)); + + git_repository_free(r2); +} diff --git a/tests-clar/submodule/status.c b/tests-clar/submodule/status.c new file mode 100644 index 000000000..e0c1e4c7a --- /dev/null +++ b/tests-clar/submodule/status.c @@ -0,0 +1,44 @@ +#include "clar_libgit2.h" +#include "posix.h" +#include "path.h" +#include "submodule_helpers.h" + +static git_repository *g_repo = NULL; + +void test_submodule_status__initialize(void) +{ + g_repo = cl_git_sandbox_init("submod2"); + + cl_fixture_sandbox("submod2_target"); + p_rename("submod2_target/.gitted", "submod2_target/.git"); + + /* must create submod2_target before rewrite so prettify will work */ + rewrite_gitmodules(git_repository_workdir(g_repo)); + p_rename("submod2/not_submodule/.gitted", "submod2/not_submodule/.git"); +} + +void test_submodule_status__cleanup(void) +{ + cl_git_sandbox_cleanup(); + cl_fixture_cleanup("submod2_target"); +} + +void test_submodule_status__unchanged(void) +{ + /* make sure it really looks unchanged */ +} + +void test_submodule_status__changed(void) +{ + /* 4 values of GIT_SUBMODULE_IGNORE to check */ + + /* 6 states of change: + * - none, (handled in __unchanged above) + * - dirty workdir file, + * - dirty index, + * - moved head, + * - untracked file, + * - missing commits (i.e. superproject commit is ahead of submodule) + */ +} + diff --git a/tests-clar/submodule/submodule_helpers.c b/tests-clar/submodule/submodule_helpers.c new file mode 100644 index 000000000..0c3e79f71 --- /dev/null +++ b/tests-clar/submodule/submodule_helpers.c @@ -0,0 +1,84 @@ +#include "clar_libgit2.h" +#include "buffer.h" +#include "path.h" +#include "util.h" +#include "posix.h" +#include "submodule_helpers.h" + +/* rewrite gitmodules -> .gitmodules + * rewrite the empty or relative urls inside each module + * rename the .gitted directory inside any submodule to .git + */ +void rewrite_gitmodules(const char *workdir) +{ + git_buf in_f = GIT_BUF_INIT, out_f = GIT_BUF_INIT, path = GIT_BUF_INIT; + FILE *in, *out; + char line[256]; + + cl_git_pass(git_buf_joinpath(&in_f, workdir, "gitmodules")); + cl_git_pass(git_buf_joinpath(&out_f, workdir, ".gitmodules")); + + cl_assert((in = fopen(in_f.ptr, "r")) != NULL); + cl_assert((out = fopen(out_f.ptr, "w")) != NULL); + + while (fgets(line, sizeof(line), in) != NULL) { + char *scan = line; + + while (*scan == ' ' || *scan == '\t') scan++; + + /* rename .gitted -> .git in submodule directories */ + if (git__prefixcmp(scan, "path =") == 0) { + scan += strlen("path ="); + while (*scan == ' ') scan++; + + git_buf_joinpath(&path, workdir, scan); + git_buf_rtrim(&path); + git_buf_joinpath(&path, path.ptr, ".gitted"); + + if (!git_buf_oom(&path) && p_access(path.ptr, F_OK) == 0) { + git_buf_joinpath(&out_f, workdir, scan); + git_buf_rtrim(&out_f); + git_buf_joinpath(&out_f, out_f.ptr, ".git"); + + if (!git_buf_oom(&out_f)) + p_rename(path.ptr, out_f.ptr); + } + } + + /* copy non-"url =" lines verbatim */ + if (git__prefixcmp(scan, "url =") != 0) { + fputs(line, out); + continue; + } + + /* convert relative URLs in "url =" lines */ + scan += strlen("url ="); + while (*scan == ' ') scan++; + + if (*scan == '.') { + git_buf_joinpath(&path, workdir, scan); + git_buf_rtrim(&path); + } else if (!*scan || *scan == '\n') { + git_buf_joinpath(&path, workdir, "../testrepo.git"); + } else { + fputs(line, out); + continue; + } + + git_path_prettify(&path, path.ptr, NULL); + git_buf_putc(&path, '\n'); + cl_assert(!git_buf_oom(&path)); + + fwrite(line, scan - line, sizeof(char), out); + fputs(path.ptr, out); + } + + fclose(in); + fclose(out); + + cl_must_pass(p_unlink(in_f.ptr)); + + git_buf_free(&in_f); + git_buf_free(&out_f); + git_buf_free(&path); +} diff --git a/tests-clar/submodule/submodule_helpers.h b/tests-clar/submodule/submodule_helpers.h new file mode 100644 index 000000000..6b76a832e --- /dev/null +++ b/tests-clar/submodule/submodule_helpers.h @@ -0,0 +1,2 @@ +extern void rewrite_gitmodules(const char *workdir); + |