1 /*
2  * libgit2 "checkout" example - shows how to perform checkouts
3  *
4  * Written by the libgit2 contributors
5  *
6  * To the extent possible under law, the author(s) have dedicated all copyright
7  * and related and neighboring rights to this software to the public domain
8  * worldwide. This software is distributed without any warranty.
9  *
10  * You should have received a copy of the CC0 Public Domain Dedication along
11  * with this software. If not, see
12  * <http://creativecommons.org/publicdomain/zero/1.0/>.
13  */
14 
15 #include "common.h"
16 
17 /* Define the printf format specifer to use for size_t output */
18 #if defined(_MSC_VER) || defined(__MINGW32__)
19 #	define PRIuZ "Iu"
20 #	define PRIxZ "Ix"
21 #	define PRIdZ "Id"
22 #else
23 #	define PRIuZ "zu"
24 #	define PRIxZ "zx"
25 #	define PRIdZ "zd"
26 #endif
27 
28 /**
29  * The following example demonstrates how to do checkouts with libgit2.
30  *
31  * Recognized options are :
32  *  --force: force the checkout to happen.
33  *  --[no-]progress: show checkout progress, on by default.
34  *  --perf: show performance data.
35  */
36 
37 typedef struct {
38 	int force : 1;
39 	int progress : 1;
40 	int perf : 1;
41 } checkout_options;
42 
print_usage(void)43 static void print_usage(void)
44 {
45 	fprintf(stderr, "usage: checkout [options] <branch>\n"
46 		"Options are :\n"
47 		"  --git-dir: use the following git repository.\n"
48 		"  --force: force the checkout.\n"
49 		"  --[no-]progress: show checkout progress.\n"
50 		"  --perf: show performance data.\n");
51 	exit(1);
52 }
53 
parse_options(const char ** repo_path,checkout_options * opts,struct args_info * args)54 static void parse_options(const char **repo_path, checkout_options *opts, struct args_info *args)
55 {
56 	if (args->argc <= 1)
57 		print_usage();
58 
59 	memset(opts, 0, sizeof(*opts));
60 
61 	/* Default values */
62 	opts->progress = 1;
63 
64 	for (args->pos = 1; args->pos < args->argc; ++args->pos) {
65 		const char *curr = args->argv[args->pos];
66 		int bool_arg;
67 
68 		if (match_arg_separator(args)) {
69 			break;
70 		} else if (!strcmp(curr, "--force")) {
71 			opts->force = 1;
72 		} else if (match_bool_arg(&bool_arg, args, "--progress")) {
73 			opts->progress = bool_arg;
74 		} else if (match_bool_arg(&bool_arg, args, "--perf")) {
75 			opts->perf = bool_arg;
76 		} else if (match_str_arg(repo_path, args, "--git-dir")) {
77 			continue;
78 		} else {
79 			break;
80 		}
81 	}
82 }
83 
84 /**
85  * This function is called to report progression, ie. it's called once with
86  * a NULL path and the number of total steps, then for each subsequent path,
87  * the current completed_step value.
88  */
print_checkout_progress(const char * path,size_t completed_steps,size_t total_steps,void * payload)89 static void print_checkout_progress(const char *path, size_t completed_steps, size_t total_steps, void *payload)
90 {
91 	(void)payload;
92 	if (path == NULL) {
93 		printf("checkout started: %" PRIuZ " steps\n", total_steps);
94 	} else {
95 		printf("checkout: %s %" PRIuZ "/%" PRIuZ "\n", path, completed_steps, total_steps);
96 	}
97 }
98 
99 /**
100  * This function is called when the checkout completes, and is used to report the
101  * number of syscalls performed.
102  */
print_perf_data(const git_checkout_perfdata * perfdata,void * payload)103 static void print_perf_data(const git_checkout_perfdata *perfdata, void *payload)
104 {
105 	(void)payload;
106 	printf("perf: stat: %" PRIuZ " mkdir: %" PRIuZ " chmod: %" PRIuZ "\n",
107 	       perfdata->stat_calls, perfdata->mkdir_calls, perfdata->chmod_calls);
108 }
109 
110 /**
111  * This is the main "checkout <branch>" function, responsible for performing
112  * a branch-based checkout.
113  */
perform_checkout_ref(git_repository * repo,git_annotated_commit * target,const char * target_ref,checkout_options * opts)114 static int perform_checkout_ref(git_repository *repo, git_annotated_commit *target, const char *target_ref, checkout_options *opts)
115 {
116 	git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT;
117 	git_reference *ref = NULL, *branch = NULL;
118 	git_commit *target_commit = NULL;
119 	int err;
120 
121 	/** Setup our checkout options from the parsed options */
122 	checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE;
123 	if (opts->force)
124 		checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE;
125 
126 	if (opts->progress)
127 		checkout_opts.progress_cb = print_checkout_progress;
128 
129 	if (opts->perf)
130 		checkout_opts.perfdata_cb = print_perf_data;
131 
132 	/** Grab the commit we're interested to move to */
133 	err = git_commit_lookup(&target_commit, repo, git_annotated_commit_id(target));
134 	if (err != 0) {
135 		fprintf(stderr, "failed to lookup commit: %s\n", git_error_last()->message);
136 		goto cleanup;
137 	}
138 
139 	/**
140 	 * Perform the checkout so the workdir corresponds to what target_commit
141 	 * contains.
142 	 *
143 	 * Note that it's okay to pass a git_commit here, because it will be
144 	 * peeled to a tree.
145 	 */
146 	err = git_checkout_tree(repo, (const git_object *)target_commit, &checkout_opts);
147 	if (err != 0) {
148 		fprintf(stderr, "failed to checkout tree: %s\n", git_error_last()->message);
149 		goto cleanup;
150 	}
151 
152 	/**
153 	 * Now that the checkout has completed, we have to update HEAD.
154 	 *
155 	 * Depending on the "origin" of target (ie. it's an OID or a branch name),
156 	 * we might need to detach HEAD.
157 	 */
158 	if (git_annotated_commit_ref(target)) {
159 		const char *target_head;
160 
161 		if ((err = git_reference_lookup(&ref, repo, git_annotated_commit_ref(target))) < 0)
162 			goto error;
163 
164 		if (git_reference_is_remote(ref)) {
165 			if ((err = git_branch_create_from_annotated(&branch, repo, target_ref, target, 0)) < 0)
166 				goto error;
167 			target_head = git_reference_name(branch);
168 		} else {
169 			target_head = git_annotated_commit_ref(target);
170 		}
171 
172 		err = git_repository_set_head(repo, target_head);
173 	} else {
174 		err = git_repository_set_head_detached_from_annotated(repo, target);
175 	}
176 
177 error:
178 	if (err != 0) {
179 		fprintf(stderr, "failed to update HEAD reference: %s\n", git_error_last()->message);
180 		goto cleanup;
181 	}
182 
183 cleanup:
184 	git_commit_free(target_commit);
185 	git_reference_free(branch);
186 	git_reference_free(ref);
187 
188 	return err;
189 }
190 
191 /**
192  * This corresponds to `git switch --guess`: if a given ref does
193  * not exist, git will by default try to guess the reference by
194  * seeing whether any remote has a branch called <ref>. If there
195  * is a single remote only that has it, then it is assumed to be
196  * the desired reference and a local branch is created for it.
197  *
198  * The following is a simplified implementation. It will not try
199  * to check whether the ref is unique across all remotes.
200  */
guess_refish(git_annotated_commit ** out,git_repository * repo,const char * ref)201 static int guess_refish(git_annotated_commit **out, git_repository *repo, const char *ref)
202 {
203 	git_strarray remotes = { NULL, 0 };
204 	git_reference *remote_ref = NULL;
205 	int error;
206 	size_t i;
207 
208 	if ((error = git_remote_list(&remotes, repo)) < 0)
209 		goto out;
210 
211 	for (i = 0; i < remotes.count; i++) {
212 		char *refname = NULL;
213 		size_t reflen;
214 
215 		reflen = snprintf(refname, 0, "refs/remotes/%s/%s", remotes.strings[i], ref);
216 		if ((refname = malloc(reflen + 1)) == NULL) {
217 			error = -1;
218 			goto next;
219 		}
220 		snprintf(refname, reflen + 1, "refs/remotes/%s/%s", remotes.strings[i], ref);
221 
222 		if ((error = git_reference_lookup(&remote_ref, repo, refname)) < 0)
223 			goto next;
224 
225 		break;
226 next:
227 		free(refname);
228 		if (error < 0 && error != GIT_ENOTFOUND)
229 			break;
230 	}
231 
232 	if (!remote_ref) {
233 		error = GIT_ENOTFOUND;
234 		goto out;
235 	}
236 
237 	if ((error = git_annotated_commit_from_ref(out, repo, remote_ref)) < 0)
238 		goto out;
239 
240 out:
241 	git_reference_free(remote_ref);
242 	git_strarray_dispose(&remotes);
243 	return error;
244 }
245 
246 /** That example's entry point */
lg2_checkout(git_repository * repo,int argc,char ** argv)247 int lg2_checkout(git_repository *repo, int argc, char **argv)
248 {
249 	struct args_info args = ARGS_INFO_INIT;
250 	checkout_options opts;
251 	git_repository_state_t state;
252 	git_annotated_commit *checkout_target = NULL;
253 	int err = 0;
254 	const char *path = ".";
255 
256 	/** Parse our command line options */
257 	parse_options(&path, &opts, &args);
258 
259 	/** Make sure we're not about to checkout while something else is going on */
260 	state = git_repository_state(repo);
261 	if (state != GIT_REPOSITORY_STATE_NONE) {
262 		fprintf(stderr, "repository is in unexpected state %d\n", state);
263 		goto cleanup;
264 	}
265 
266 	if (match_arg_separator(&args)) {
267 		/**
268 		 * Try to checkout the given path
269 		 */
270 
271 		fprintf(stderr, "unhandled path-based checkout\n");
272 		err = 1;
273 		goto cleanup;
274 	} else {
275 		/**
276 		 * Try to resolve a "refish" argument to a target libgit2 can use
277 		 */
278 		if ((err = resolve_refish(&checkout_target, repo, args.argv[args.pos])) < 0 &&
279 		    (err = guess_refish(&checkout_target, repo, args.argv[args.pos])) < 0) {
280 			fprintf(stderr, "failed to resolve %s: %s\n", args.argv[args.pos], git_error_last()->message);
281 			goto cleanup;
282 		}
283 		err = perform_checkout_ref(repo, checkout_target, args.argv[args.pos], &opts);
284 	}
285 
286 cleanup:
287 	git_annotated_commit_free(checkout_target);
288 
289 	return err;
290 }
291