-
Notifications
You must be signed in to change notification settings - Fork 286
/
Copy pathcreate.R
398 lines (353 loc) · 14 KB
/
create.R
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
#' Create a package or project
#'
#' @description
#' These functions create an R project:
#' * `create_package()` creates an R package
#' * `create_project()` creates a non-package project, i.e. a data analysis
#' project
#' * `create_quarto_project()` creates a Quarto project
#'
#' Both functions can be called on an existing project; you will be asked before
#' any existing files are changed.
#'
#' @inheritParams use_description
#' @param fields A named list of fields to add to `DESCRIPTION`, potentially
#' overriding default values. See [use_description()] for how you can set
#' personalized defaults using package options.
#' @param path A path. If it exists, it is used. If it does not exist, it is
#' created, provided that the parent path exists.
#' @param roxygen Do you plan to use roxygen2 to document your package?
#' @param rstudio If `TRUE`, calls [use_rstudio()] to make the new package or
#' project into an [RStudio
#' Project](https://r-pkgs.org/workflow101.html#sec-workflow101-rstudio-projects).
#' If `FALSE` and a non-package project, a sentinel `.here` file is placed so
#' that the directory can be recognized as a project by the
#' [here](https://here.r-lib.org) or
#' [rprojroot](https://rprojroot.r-lib.org) packages.
#' @param open If `TRUE`, [activates][proj_activate()] the new project:
#'
#' * If using RStudio desktop, the package is opened in a new session.
#' * If on RStudio server, the current RStudio project is activated.
#' * Otherwise, the working directory and active project is changed.
#' @param ... pass quarto_create_project aditional arguments.
#' @param name name for quarto project folder
#' @return Path to the newly created project or package, invisibly.
#' @seealso [create_tidy_package()] is a convenience function that extends
#' `create_package()` by immediately applying as many of the tidyverse
#' development conventions as possible.
#' @export
create_package <- function(path,
fields = list(),
rstudio = rstudioapi::isAvailable(),
roxygen = TRUE,
check_name = TRUE,
open = rlang::is_interactive()) {
path <- user_path_prep(path)
check_path_is_directory(path_dir(path))
name <- path_file(path_abs(path))
if (check_name) {
check_package_name(name)
}
challenge_nested_project(path_dir(path), name)
challenge_home_directory(path)
create_directory(path)
local_project(path, force = TRUE)
use_directory("R")
proj_desc_create(name, fields, roxygen)
use_namespace(roxygen = roxygen)
if (rstudio) {
use_rstudio()
}
if (open) {
if (proj_activate(proj_get())) {
# working directory/active project already set; clear the scheduled
# restoration of the original project
withr::deferred_clear()
}
}
invisible(proj_get())
}
#' @export
#' @rdname create_package
create_project <- function(path,
rstudio = rstudioapi::isAvailable(),
open = rlang::is_interactive()) {
path <- user_path_prep(path)
name <- path_file(path_abs(path))
challenge_nested_project(path_dir(path), name)
challenge_home_directory(path)
create_directory(path)
local_project(path, force = TRUE)
use_directory("R")
if (rstudio) {
use_rstudio()
} else {
ui_bullets(c(
"v" = "Writing a sentinel file {.path {pth('.here')}}.",
"_" = "Build robust paths within your project via {.fun here::here}.",
"i" = "Learn more at {.url https://here.r-lib.org}."
))
file_create(proj_path(".here"))
}
if (open) {
if (proj_activate(proj_get())) {
# working directory/active project already set; clear the scheduled
# restoration of the original project
withr::deferred_clear()
}
}
invisible(proj_get())
}
#' @rdname create_package
#' @export
create_quarto_project <- function(name, ...) {
rlang::check_installed("quarto", reason = "to use `quarto_create_project()`")
quarto::quarto_create_project(name = name, ...)
}
#' Create a project from a GitHub repo
#'
#' @description
#' Creates a new local project and Git repository from a repo on GitHub, by
#' either cloning or
#' [fork-and-cloning](https://docs.github.com/en/get-started/quickstart/fork-a-repo).
#' In the fork-and-clone case, `create_from_github()` also does additional
#' remote and branch setup, leaving you in the perfect position to make a pull
#' request with [pr_init()], one of several [functions for working with pull
#' requests][pull-requests].
#'
#' `create_from_github()` works best when your GitHub credentials are
#' discoverable. See below for more about authentication.
#'
#' @template double-auth
#'
#' @seealso
#' * [use_github()] to go the opposite direction, i.e. create a GitHub repo
#' from your local repo
#' * [git_protocol()] for background on `protocol` (HTTPS vs SSH)
#' * [use_course()] to download a snapshot of all files in a GitHub repo,
#' without the need for any local or remote Git operations
#'
#' @inheritParams create_package
#' @param repo_spec A string identifying the GitHub repo in one of these forms:
#' * Plain `OWNER/REPO` spec
#' * Browser URL, such as `"https://github.com/OWNER/REPO"`
#' * HTTPS Git URL, such as `"https://github.com/OWNER/REPO.git"`
#' * SSH Git URL, such as `"[email protected]:OWNER/REPO.git"`
#' @param destdir Destination for the new folder, which will be named according
#' to the `REPO` extracted from `repo_spec`. Defaults to the location stored
#' in the global option `usethis.destdir`, if defined, or to the user's
#' Desktop or similarly conspicuous place otherwise.
#' @param fork If `FALSE`, we clone `repo_spec`. If `TRUE`, we fork
#' `repo_spec`, clone that fork, and do additional setup favorable for
#' future pull requests:
#' * The source repo, `repo_spec`, is configured as the `upstream` remote,
#' using the indicated `protocol`.
#' * The local `DEFAULT` branch is set to track `upstream/DEFAULT`, where
#' `DEFAULT` is typically `main` or `master`. It is also immediately pulled,
#' to cover the case of a pre-existing, out-of-date fork.
#'
#' If `fork = NA` (the default), we check your permissions on `repo_spec`. If
#' you can push, we set `fork = FALSE`, If you cannot, we set `fork = TRUE`.
#' @param host GitHub host to target, passed to the `.api_url` argument of
#' [gh::gh()]. If `repo_spec` is a URL, `host` is extracted from that.
#'
#' If unspecified, gh defaults to "https://api.github.com", although gh's
#' default can be customised by setting the GITHUB_API_URL environment
#' variable.
#'
#' For a hypothetical GitHub Enterprise instance, either
#' "https://github.acme.com/api/v3" or "https://github.acme.com" is
#' acceptable.
#' @param rstudio Initiate an [RStudio
#' Project](https://r-pkgs.org/workflow101.html#sec-workflow101-rstudio-projects)?
#' Defaults to `TRUE` if in an RStudio session and project has no
#' pre-existing `.Rproj` file. Defaults to `FALSE` otherwise (but note that
#' the cloned repo may already be an RStudio Project, i.e. may already have a
#' `.Rproj` file).
#' @inheritParams use_github
#'
#' @export
#' @examples
#' \dontrun{
#' create_from_github("r-lib/usethis")
#'
#' # repo_spec can be a URL
#' create_from_github("https://github.com/r-lib/usethis")
#'
#' # a URL repo_spec also specifies the host (e.g. GitHub Enterprise instance)
#' create_from_github("https://github.acme.com/OWNER/REPO")
#' }
create_from_github <- function(repo_spec,
destdir = NULL,
fork = NA,
rstudio = NULL,
open = rlang::is_interactive(),
protocol = git_protocol(),
host = NULL) {
check_protocol(protocol)
parsed_repo_spec <- parse_repo_url(repo_spec)
if (!is.null(parsed_repo_spec$host)) {
repo_spec <- parsed_repo_spec$repo_spec
host <- parsed_repo_spec$host
}
whoami <- suppressMessages(gh::gh_whoami(.api_url = host))
no_auth <- is.null(whoami)
user <- if (no_auth) NULL else whoami$login
hint <- code_hint_with_host("gh_token_help", host)
if (no_auth && is.na(fork)) {
ui_abort(c(
"x" = "Unable to discover a GitHub personal access token.",
"x" = "Therefore, can't determine your permissions on {.val {repo_spec}}.",
"x" = "Therefore, can't decide if {.arg fork} should be {.code TRUE} or {.code FALSE}.",
"",
"i" = "You have two choices:",
"_" = "Make your token available (if in doubt, DO THIS):",
" " = "Call {.code {hint}} for instructions that should help.",
"_" = "Call {.fun create_from_github} again, but with {.code fork = FALSE}.",
" " = "Only do this if you are absolutely sure you don't want to fork.",
" " = "Note you will NOT be in a position to make a pull request."
))
}
if (no_auth && isTRUE(fork)) {
ui_abort(c(
"x" = "Unable to discover a GitHub personal access token.",
"i" = "A token is required in order to fork {.val {repo_spec}}.",
"_" = "Call {.code {hint}} for help configuring a token."
))
}
# one of these is true:
# - gh is discovering a token for `host`
# - gh is NOT discovering a token, but `fork = FALSE`, so that's OK
source_owner <- spec_owner(repo_spec)
repo_name <- spec_repo(repo_spec)
gh <- gh_tr(list(repo_owner = source_owner, repo_name = repo_name, api_url = host))
repo_info <- gh("GET /repos/{owner}/{repo}")
# 2023-01-28 We're seeing the GitHub bug again around default branch in a
# fresh fork. If I create a fork, the POST payload *sometimes* mis-reports the
# default branch. I.e. it reports `main`, even though the actual default
# branch is `master`. Therefore we're reverting to consulting the source repo
# for this info
default_branch <- repo_info$default_branch
if (is.na(fork)) {
fork <- !isTRUE(repo_info$permissions$push)
fork_status <- glue("fork = {fork}")
ui_bullets(c("v" = "Setting {.code {fork_status}}."))
}
# fork is either TRUE or FALSE
if (fork && identical(user, repo_info$owner$login)) {
ui_abort("
Can't fork, because the authenticated user {.val {user}} already owns the
source repo {.val {repo_info$full_name}}.")
}
destdir <- user_path_prep(destdir %||% conspicuous_place())
check_path_is_directory(destdir)
challenge_nested_project(destdir, repo_name)
repo_path <- path(destdir, repo_name)
create_directory(repo_path)
check_directory_is_empty(repo_path)
if (fork) {
## https://developer.github.com/v3/repos/forks/#create-a-fork
ui_bullets(c("v" = "Forking {.val {repo_info$full_name}}."))
upstream_url <- switch(
protocol,
https = repo_info$clone_url,
ssh = repo_info$ssh_url
)
repo_info <- gh("POST /repos/{owner}/{repo}/forks")
ui_bullets(c("i" = "Waiting for the fork to finalize before cloning..."))
Sys.sleep(3)
}
origin_url <- switch(
protocol,
https = repo_info$clone_url,
ssh = repo_info$ssh_url
)
ui_bullets(c(
"v" = "Cloning repo from {.val {origin_url}} into {.path {repo_path}}."
))
gert::git_clone(origin_url, repo_path, verbose = FALSE)
proj_path <- find_rstudio_root(repo_path)
local_project(proj_path, force = TRUE) # schedule restoration of project
# 2023-01-28 again, it would be more natural to trust the default branch of
# the fork, but that cannot always be trusted. For now, we're still using
# the default branch learned from the source repo.
ui_bullets(c("i" = "Default branch is {.val {default_branch}}."))
if (fork) {
ui_bullets(c(
"v" = "Adding {.val upstream} remote: {.val {upstream_url}}"
))
use_git_remote("upstream", upstream_url)
pr_merge_main()
upstream_remref <- glue("upstream/{default_branch}")
ui_bullets(c(
"v" = "Setting remote tracking branch for local {.val {default_branch}}
branch to {.val {upstream_remref}}."
))
gert::git_branch_set_upstream(upstream_remref, repo = git_repo())
config_key <- glue("remote.upstream.created-by")
gert::git_config_set(config_key, "usethis::create_from_github", repo = git_repo())
}
rstudio <- rstudio %||% rstudio_available()
rstudio <- rstudio && !is_rstudio_project()
if (rstudio) {
use_rstudio(reformat = FALSE)
}
if (open) {
if (proj_activate(proj_get())) {
# Working directory/active project changed; so don't undo on exit
withr::deferred_clear()
}
}
invisible(proj_get())
}
# If there's a single directory containing an .Rproj file, use it.
# Otherwise work in the repo root
find_rstudio_root <- function(path) {
rproj <- rproj_paths(path, recurse = TRUE)
if (length(rproj) == 1) {
path_dir(rproj)
} else {
path
}
}
challenge_nested_project <- function(path, name) {
if (!possibly_in_proj(path)) {
return(invisible())
}
# creates an undocumented backdoor we can exploit when the interactive
# approval is impractical, e.g. in tests
if (isTRUE(getOption("usethis.allow_nested_project", FALSE))) {
return(invisible())
}
ui_bullets(c(
"!" = "New project {.val {name}} is nested inside an existing project
{.path {pth(path)}}, which is rarely a good idea.",
"i" = "If this is unexpected, the {.pkg here} package has a function,
{.fun here::dr_here} that reveals why {.path {pth(path)}} is regarded
as a project."
))
if (ui_nah("Do you want to create anyway?")) {
ui_abort("Cancelling project creation.")
}
invisible()
}
challenge_home_directory <- function(path) {
homes <- unique(c(path_home(), path_home_r()))
if (!path %in% homes) {
return(invisible())
}
qualification <- if (is_windows()) {
glue("a special directory, i.e. some applications regard it as ")
} else {
""
}
ui_bullets(c(
"!" = "{.path {pth(path)}} is {qualification}your home directory.",
"i" = "It is generally a bad idea to create a new project here.",
"i" = "You should probably create your new project in a subdirectory."
))
if (ui_nah("Do you want to create anyway?")) {
ui_abort("Good move! Cancelling project creation.")
}
invisible()
}