making-with-code-cli 5.2.1__py3-none-any.whl → 5.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,173 @@
1
+ """Shared spec and validation for assessment.md.
2
+
3
+ A student module repo may contain `assessment.md`, written by a teacher (by hand or
4
+ with a script -- assessment *tooling* is being designed and built separately from this
5
+ spec) to record how the module was assessed. The file has TOML front matter (delimited
6
+ with `+++`, matching the convention MWC's own site uses for module front matter)
7
+ holding the *current* assessment -- evergreen, not a history. Re-assessment overwrites
8
+ the front matter in place; the running history of what changed and why is written by
9
+ the teacher as prose in the file's body (a letter to the student, in the style of the
10
+ assessment-rubric skill), not as structured data.
11
+
12
+ This module is the single place that knows the shape of that front matter, so that
13
+ every reader and writer of `assessment.md` -- `mwc status` (student-facing),
14
+ `mwc teach status` (teacher-facing), any future assessment tooling -- agrees on it and
15
+ validates against the same rules, rather than each parsing it their own way.
16
+
17
+ The front matter always has all four keys:
18
+ - `assessment_type`: `"completion"` or `"score"`
19
+ - `timestamp`: when the current assessment happened (authoritative for "how recent is
20
+ this assessment" -- deliberately the TOML value, not any git commit metadata, so
21
+ everything relevant stays in-band in the file itself)
22
+ - `score`: an integer, `0 <= score <= points_possible`
23
+ - `points_possible`: an integer; always `1` for `"completion"` (so score is `0` or `1`)
24
+
25
+ Rubric-based assessment doesn't get its own structured type -- the rubric detail
26
+ (per-criterion feedback) lives in the prose body, and the front matter just records
27
+ a summary `"score"` that downstream consumers can turn into a grade.
28
+
29
+ +++
30
+ assessment_type = "score"
31
+ timestamp = 2026-03-15T09:30:00Z
32
+ score = 8
33
+ points_possible = 10
34
+ +++
35
+
36
+ Dear student, ...
37
+ """
38
+ from dataclasses import dataclass
39
+ from datetime import datetime
40
+ from numbers import Real
41
+
42
+ import tomlkit
43
+ from tomlkit.exceptions import TOMLKitError
44
+
45
+ from making_with_code_cli.errors import MWCError
46
+
47
+ FRONT_MATTER_DELIMITER = "+++"
48
+ ASSESSMENT_TYPES = ("completion", "score")
49
+
50
+
51
+ class AssessmentValidationError(MWCError):
52
+ """Raised when assessment.md front matter doesn't conform to the spec."""
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class Assessment:
57
+ "A module's current assessment, evergreen (overwritten on re-assessment)."
58
+ assessment_type: str # "completion" | "score"
59
+ timestamp: datetime
60
+ score: int
61
+ points_possible: int
62
+
63
+
64
+ def extract_front_matter(markdown_text):
65
+ """Splits a +++...+++-delimited file into (front_matter_toml, body_markdown).
66
+ Returns ("", markdown_text) if there's no front matter at all -- a module that
67
+ hasn't been assessed yet may have no assessment.md, or an assessment.md with no
68
+ front matter yet, and that should parse as "not assessed," not an error.
69
+ """
70
+ lines = markdown_text.splitlines(keepends=True)
71
+ if not lines or lines[0].strip() != FRONT_MATTER_DELIMITER:
72
+ return "", markdown_text
73
+ for i, line in enumerate(lines[1:], start=1):
74
+ if line.strip() == FRONT_MATTER_DELIMITER:
75
+ return "".join(lines[1:i]), "".join(lines[i + 1:])
76
+ # Opening delimiter with no closing delimiter -- treat as malformed, not fatal;
77
+ # callers just get "not assessed" back rather than a crash.
78
+ return "", markdown_text
79
+
80
+
81
+ def parse_assessment(toml_text):
82
+ """Parses assessment.md's front matter into an Assessment, or None if it's
83
+ empty/missing `assessment_type` (not assessed yet -- a valid, unremarkable
84
+ state, not an error). Raises AssessmentValidationError if `assessment_type`
85
+ is present but the rest of the data doesn't conform to the spec.
86
+ """
87
+ if not toml_text.strip():
88
+ return None
89
+ try:
90
+ doc = tomlkit.parse(toml_text)
91
+ except TOMLKitError as e:
92
+ raise AssessmentValidationError(f"assessment.md front matter is not valid TOML: {e}")
93
+
94
+ if "assessment_type" not in doc:
95
+ return None
96
+ return _parse_assessment_table(doc)
97
+
98
+
99
+ def parse_assessment_file(markdown_text):
100
+ """extract_front_matter + parse_assessment, for reading a whole assessment.md
101
+ file (its text content, not a path -- callers decide whether that text comes
102
+ from the working tree or `git show <ref>:assessment.md`)."""
103
+ front_matter, _ = extract_front_matter(markdown_text)
104
+ return parse_assessment(front_matter)
105
+
106
+
107
+ def format_score(assessment):
108
+ """Compact single-line rendering of an assessment's score, for table display.
109
+ ASCII only (no unicode symbols), matching mwc teach status's convention.
110
+ """
111
+ if assessment.assessment_type == "completion":
112
+ return "Y" if assessment.score else "N"
113
+ return f"{assessment.score}/{assessment.points_possible}"
114
+
115
+
116
+ def set_assessment(markdown_text, assessment):
117
+ """Returns markdown_text with its front matter replaced by `assessment`
118
+ (creating the front matter if markdown_text has none). The body is left
119
+ untouched -- the teacher writes their own log of the (re-)assessment there;
120
+ this function only ever owns the evergreen front matter, never the prose.
121
+ Uses tomlkit so any other keys already in the front matter are preserved.
122
+ """
123
+ front_matter, body = extract_front_matter(markdown_text)
124
+ doc = tomlkit.parse(front_matter) if front_matter.strip() else tomlkit.document()
125
+ doc["assessment_type"] = assessment.assessment_type
126
+ doc["timestamp"] = assessment.timestamp
127
+ doc["score"] = assessment.score
128
+ doc["points_possible"] = assessment.points_possible
129
+ new_front_matter = tomlkit.dumps(doc)
130
+ return f"{FRONT_MATTER_DELIMITER}\n{new_front_matter}{FRONT_MATTER_DELIMITER}\n{body}"
131
+
132
+
133
+ def _parse_assessment_table(raw):
134
+ "Validates and converts a raw TOML mapping into an Assessment."
135
+ assessment_type = raw.get("assessment_type")
136
+ if assessment_type not in ASSESSMENT_TYPES:
137
+ raise AssessmentValidationError(
138
+ f"assessment_type must be one of {ASSESSMENT_TYPES}, got {assessment_type!r}"
139
+ )
140
+
141
+ timestamp = raw.get("timestamp")
142
+ if not isinstance(timestamp, datetime):
143
+ raise AssessmentValidationError("timestamp is required and must be a date/time")
144
+
145
+ if "score" not in raw:
146
+ raise AssessmentValidationError("score is required")
147
+ score = raw["score"]
148
+ if isinstance(score, bool) or not isinstance(score, Real):
149
+ raise AssessmentValidationError(f"score must be a number, got {score!r}")
150
+
151
+ if "points_possible" not in raw:
152
+ raise AssessmentValidationError("points_possible is required")
153
+ points_possible = raw["points_possible"]
154
+ if isinstance(points_possible, bool) or not isinstance(points_possible, Real):
155
+ raise AssessmentValidationError(f"points_possible must be a number, got {points_possible!r}")
156
+ if points_possible <= 0:
157
+ raise AssessmentValidationError("points_possible must be positive")
158
+
159
+ if assessment_type == "completion" and points_possible != 1:
160
+ raise AssessmentValidationError(
161
+ f"points_possible must be 1 for a completion assessment, got {points_possible!r}"
162
+ )
163
+ if not (0 <= score <= points_possible):
164
+ raise AssessmentValidationError(
165
+ f"score ({score}) must be between 0 and points_possible ({points_possible})"
166
+ )
167
+
168
+ return Assessment(
169
+ assessment_type=assessment_type,
170
+ timestamp=timestamp,
171
+ score=score,
172
+ points_possible=points_possible,
173
+ )
@@ -3,6 +3,7 @@ from making_with_code_cli.version import version
3
3
  from making_with_code_cli.setup import setup
4
4
  from making_with_code_cli.update import update
5
5
  from making_with_code_cli.submit import submit
6
+ from making_with_code_cli.status import status
6
7
  from making_with_code_cli.teach import teach
7
8
 
8
9
  @click.group()
@@ -13,4 +14,5 @@ cli.add_command(version)
13
14
  cli.add_command(setup)
14
15
  cli.add_command(update)
15
16
  cli.add_command(submit)
17
+ cli.add_command(status)
16
18
  cli.add_command(teach)
@@ -11,7 +11,7 @@ def get_curriculum(mwc_site_url, course_name=None):
11
11
  """Fetches curriculum metadata from the site url specified in settings.
12
12
  Returns the curriculum metadata for course_name.
13
13
  """
14
- url = mwc_site_url + "/manifest"
14
+ url = mwc_site_url.rstrip("/") + "/manifest"
15
15
  try:
16
16
  response = requests.get(url, timeout=15)
17
17
  except requests.exceptions.RequestException as e:
@@ -26,3 +26,15 @@ class SoftwareInstallationError(MWCError):
26
26
 
27
27
  class NoCurriculaAvailable(MWCError):
28
28
  pass
29
+
30
+ class GitError(MWCError):
31
+ pass
32
+
33
+ class GitNetworkError(GitError):
34
+ pass
35
+
36
+ class GitAuthError(GitError):
37
+ pass
38
+
39
+ class GitCorruptError(GitError):
40
+ pass
@@ -2,7 +2,6 @@ from pathlib import Path
2
2
  from subprocess import run, CalledProcessError
3
3
  import click
4
4
  from making_with_code_cli.helpers import cd
5
- from making_with_code_cli.setup.tasks import WORK_DIR_PERMISSIONS
6
5
  from making_with_code_cli.styles import (
7
6
  address,
8
7
  info,
@@ -1,5 +1,6 @@
1
1
  from .base_backend import GitBackend
2
2
  from subprocess import run, CalledProcessError
3
+ import re
3
4
  import traceback
4
5
  from pathlib import Path
5
6
  from urllib.parse import urlparse
@@ -8,6 +9,12 @@ import json
8
9
  import os
9
10
  import requests
10
11
  from making_with_code_cli.helpers import cd
12
+ from making_with_code_cli.git_wrapper import (
13
+ abort_in_progress_operation,
14
+ recover_detached_head,
15
+ clear_stale_index_lock,
16
+ _get_default_branch,
17
+ )
11
18
  from making_with_code_cli.styles import (
12
19
  address,
13
20
  confirm,
@@ -18,6 +25,10 @@ from making_with_code_cli.styles import (
18
25
  from making_with_code_cli.errors import (
19
26
  MissingSetting,
20
27
  GitServerNotAvailable,
28
+ GitError,
29
+ GitNetworkError,
30
+ GitAuthError,
31
+ GitCorruptError,
21
32
  )
22
33
 
23
34
  EDITOR_COMMANDS = {
@@ -29,17 +40,55 @@ EDITOR_COMMANDS = {
29
40
  "emacs": "emacs",
30
41
  }
31
42
 
43
+ # Phrases in git stderr that identify each failure mode
44
+ _NETWORK_PHRASES = [
45
+ "Could not resolve host",
46
+ "Connection timed out",
47
+ "unable to connect",
48
+ "Failed to connect",
49
+ "network is unreachable",
50
+ "Connection refused",
51
+ ]
52
+ _AUTH_PHRASES = [
53
+ "Authentication failed",
54
+ "403",
55
+ "invalid credentials",
56
+ "could not read Username",
57
+ "terminal prompts disabled",
58
+ "Repository not found",
59
+ ]
60
+ _CORRUPT_PHRASES = [
61
+ "corrupt",
62
+ "bad object",
63
+ "loose object",
64
+ "fatal: bad",
65
+ "object file is empty",
66
+ ]
67
+ _NO_UPSTREAM_PHRASES = [
68
+ "no tracking information",
69
+ "has no upstream",
70
+ "There is no tracking information",
71
+ ]
72
+
73
+
32
74
  class MWCBackend(GitBackend):
33
- """A Github backend. Students own their own repos and grant teachers access via token.
34
- """
75
+ """A Github backend. Students own their own repos and grant teachers access via token."""
35
76
 
36
77
  MWC_GIT_PROTOCOL = "https"
37
78
  MWC_GIT_SERVER = "git.makingwithcode.org"
38
79
  COMMIT_TEMPLATE = ".commit_template"
39
80
 
81
+ @property
82
+ def git_server(self):
83
+ """Returns the git server hostname, respecting git_server_url setting if set."""
84
+ url = self.settings.get('git_server_url')
85
+ if url:
86
+ parsed = urlparse(url)
87
+ return parsed.netloc
88
+ return self.MWC_GIT_SERVER
89
+
40
90
  def init_module(self, module, modpath):
41
- """Creates the named repo from a template.
42
- """
91
+ """Creates the named repo from a template."""
43
92
  self.check_settings()
44
93
  server, repo_owner, repo_name = self.parse_repo_url(module["repo_url"])
45
94
 
@@ -53,8 +102,9 @@ class MWCBackend(GitBackend):
53
102
  if (modpath / self.COMMIT_TEMPLATE).exists():
54
103
  run(["git", "config", "--local", "commit.template", self.COMMIT_TEMPLATE],
55
104
  check=True, cwd=modpath)
56
- run("uv venv", shell=True, check=True, cwd=modpath, capture_output=True)
57
- self.init_direnv(modpath)
105
+ if self.settings.get('manage_python_environment', True):
106
+ run("uv venv", shell=True, check=True, cwd=modpath, capture_output=True)
107
+ self.init_direnv(modpath)
58
108
 
59
109
  def configure_git(self, modpath):
60
110
  """Configures git user settings locally for the repo."""
@@ -62,41 +112,199 @@ class MWCBackend(GitBackend):
62
112
  run(["git", "config", "--local", "user.name", name], check=True, cwd=modpath)
63
113
  run(["git", "config", "--local", "user.email", "nobody@makingwithcode.org"],
64
114
  check=True, cwd=modpath)
115
+ # EDITOR_COMMANDS covers the GUI editors that need an explicit "wait for
116
+ # the tab to close" flag. Anything else the student typed at setup was
117
+ # already verified to resolve on $PATH (see choose_editor), so pass it
118
+ # through as-is -- students should be able to use editors we don't know
119
+ # about by name. Only fall back to nano if editor is somehow unset.
65
120
  editor = self.settings.get('editor')
66
- if editor in EDITOR_COMMANDS:
67
- run(["git", "config", "--local", "core.editor", EDITOR_COMMANDS[editor]],
68
- check=True, cwd=modpath)
121
+ editor_command = EDITOR_COMMANDS.get(editor, editor) or "nano"
122
+ run(["git", "config", "--local", "core.editor", editor_command],
123
+ check=True, cwd=modpath)
69
124
 
70
125
  def update(self, module, modpath, install=True):
71
126
  if (modpath / ".git").is_dir():
72
- with cd(modpath):
73
- relpath = self.relative_path(modpath)
74
- click.echo(address(f"Checking {relpath} for updates.", preformatted=True))
75
- gitresult = run("git pull", shell=True, capture_output=True, text=True)
76
- if gitresult.returncode != 0:
77
- click.echo(error(
78
- f"Could not update {relpath}: {gitresult.stderr.strip()}\n"
79
- "You may have local changes that conflict. Ask a teacher for help."
80
- ))
81
- return
82
- click.echo(info(gitresult.stdout))
83
- if install and Path("pyproject.toml").exists():
84
- result = run("uv sync", shell=True, capture_output=True, text=True, cwd=modpath)
85
- if result.returncode != 0:
86
- click.echo(debug("Error running uv sync:"))
87
- click.echo(debug(result.stderr, preformatted=True))
88
- click.echo(error(f"There was a problem updating {relpath}. Ask a teacher."))
89
- else:
90
- self.init_direnv(modpath)
127
+ # Idempotent -- re-applies user/editor config so repos created
128
+ # before this config existed (or with a stale editor choice)
129
+ # still get it.
130
+ self.configure_git(modpath)
131
+ relpath = self.relative_path(modpath)
132
+ click.echo(address(f"Checking {relpath} for updates.", preformatted=True))
133
+ pull_output = self.safe_pull(modpath)
134
+ if pull_output:
135
+ click.echo(info(pull_output))
136
+ if install and (modpath / "pyproject.toml").exists() and self.settings.get('manage_python_environment', True):
137
+ result = run("uv sync", shell=True, capture_output=True, text=True, cwd=modpath)
138
+ if result.returncode != 0:
139
+ click.echo(debug("Error running uv sync:"))
140
+ click.echo(debug(result.stderr, preformatted=True))
141
+ click.echo(error(f"There was a problem updating {relpath}. Ask a teacher."))
142
+ else:
143
+ self.init_direnv(modpath)
144
+
145
+ def safe_pull(self, modpath):
146
+ """Pulls from origin with robust recovery for common git error states.
147
+ Returns the git output string on success. Raises GitError subclasses on
148
+ unrecoverable failures.
149
+ """
150
+ # Fix bad repo states before attempting the pull
151
+ aborted = abort_in_progress_operation(modpath)
152
+ if aborted:
153
+ click.echo(confirm(
154
+ f"Cleared an incomplete git {aborted} — this shouldn't affect your work."
155
+ ))
156
+
157
+ recovered = recover_detached_head(modpath)
158
+ if recovered:
159
+ branch, stash_unresolved = recovered
160
+ click.echo(confirm(
161
+ f"Your repo was not on any branch. Switched back to '{branch}'."
162
+ ))
163
+ if stash_unresolved:
164
+ click.echo(error(
165
+ "Some of your uncommitted work couldn't be automatically restored "
166
+ "and is saved in a git stash instead of your files. "
167
+ "Ask your teacher for help recovering it."
168
+ ))
169
+
170
+ clear_stale_index_lock(modpath)
171
+
172
+ run("git fetch origin", shell=True, capture_output=True, cwd=modpath)
173
+
174
+ # Detect (without applying) any genuine content conflicts between
175
+ # FETCH_HEAD and the repo's current state -- both the working tree
176
+ # (if dirty) and the student's committed history. -X ours/-X theirs
177
+ # auto-resolve conflicts silently, so this is the only way to know
178
+ # when that's about to happen.
179
+ conflicted = self._conflicting_files(modpath, "FETCH_HEAD")
180
+
181
+ # U1: uncommitted changes that would genuinely conflict with the
182
+ # update — cancel and have the student save their own work first,
183
+ # rather than guessing at a commit message on their behalf. Handling
184
+ # one thing at a time (submit, then update) is clearer for students
185
+ # than a merge that silently combines both, and avoids a commit
186
+ # showing up in their history that they didn't write.
187
+ status = run(
188
+ "git status --porcelain",
189
+ shell=True, capture_output=True, text=True, cwd=modpath
190
+ )
191
+ if status.stdout.strip() and conflicted:
192
+ raise GitError(
193
+ "You have unsaved changes here that would conflict with an "
194
+ "update. Run `mwc submit` to save your work, then run "
195
+ "`mwc update` again."
196
+ )
197
+
198
+ result = self._run_pull(modpath)
199
+
200
+ if result.returncode == 0:
201
+ if conflicted:
202
+ files = ", ".join(f"`{f}`" for f in conflicted)
203
+ click.echo(error(
204
+ f"Your changes to {files} conflicted with an update — the "
205
+ "update's version was kept for the conflicting lines. Ask "
206
+ "your teacher if you're not sure that's right."
207
+ ))
208
+ return result.stdout
209
+
210
+ combined = result.stderr + result.stdout
211
+
212
+ # U8: no upstream tracking branch — set it and retry
213
+ if any(p in combined for p in _NO_UPSTREAM_PHRASES):
214
+ default_branch = _get_default_branch(modpath)
215
+ run(
216
+ f"git branch --set-upstream-to=origin/{default_branch} {default_branch}",
217
+ shell=True, capture_output=True, cwd=modpath
218
+ )
219
+ result = self._run_pull(modpath)
220
+ if result.returncode == 0:
221
+ return result.stdout
222
+ combined = result.stderr + result.stdout
223
+
224
+ click.echo(debug(combined.strip(), preformatted=True))
225
+ self._raise_pull_error(combined)
226
+
227
+ def _run_pull(self, modpath):
228
+ # -X theirs resolves merge conflicts in favor of incoming (teacher) changes,
229
+ # and also handles diverged-history cases (U2, U3) without extra configuration.
230
+ return run(
231
+ "git pull -X theirs",
232
+ shell=True, capture_output=True, text=True, cwd=modpath
233
+ )
234
+
235
+ def _conflicting_files(self, modpath, ref):
236
+ """Returns the list of files that can't be cleanly auto-merged when
237
+ merging `ref` into HEAD, without actually applying or committing
238
+ anything. -X ours/-X theirs auto-resolve conflicts and don't report
239
+ them, so this is the only way to tell whether an automatic merge is
240
+ about to silently pick a side on real conflicting content (as opposed
241
+ to just cleanly combining separate, non-overlapping changes).
242
+
243
+ Covers two distinct git failure modes: real textual conflicts
244
+ (reported as "CONFLICT", with the files showing up in
245
+ `git diff --diff-filter=U`), and a dirty working tree touching the
246
+ same file as the incoming change (git refuses outright with "local
247
+ changes ... would be overwritten by merge" and never starts a merge
248
+ at all, so there's nothing in the index to look up -- the filenames
249
+ have to be parsed out of that message instead).
250
+ """
251
+ merge_check = run(
252
+ f"git merge --no-commit --no-ff {ref}",
253
+ shell=True, capture_output=True, text=True, cwd=modpath
254
+ )
255
+ conflicts = []
256
+ if merge_check.returncode != 0:
257
+ diff = run(
258
+ "git diff --name-only --diff-filter=U",
259
+ shell=True, capture_output=True, text=True, cwd=modpath
260
+ )
261
+ conflicts = [f for f in diff.stdout.splitlines() if f.strip()]
262
+ if not conflicts:
263
+ combined = merge_check.stdout + merge_check.stderr
264
+ conflicts = re.findall(r"^\t(.+)$", combined, re.MULTILINE)
265
+ # Always abort the probe merge so we start clean. This is a no-op
266
+ # (and prints a harmless "no merge to abort" to stderr, which we
267
+ # discard) in the "would be overwritten" case, since no merge ever
268
+ # started there.
269
+ run("git merge --abort", shell=True, capture_output=True, cwd=modpath)
270
+ return conflicts
271
+
272
+ def _raise_pull_error(self, stderr):
273
+ if any(p in stderr for p in _NETWORK_PHRASES):
274
+ raise GitNetworkError(
275
+ "Could not connect to the MWC server. "
276
+ "Check your internet connection and try again."
277
+ )
278
+ if any(p in stderr for p in _AUTH_PHRASES):
279
+ raise GitAuthError(
280
+ "Your login credentials are no longer valid. "
281
+ "Run `mwc setup` to refresh them, then run `mwc update` again."
282
+ )
283
+ if any(p in stderr for p in _CORRUPT_PHRASES):
284
+ raise GitCorruptError(
285
+ "Your local copy of this module appears to be damaged. "
286
+ "Ask your teacher for help."
287
+ )
288
+ raise GitError(
289
+ "Could not update this module. "
290
+ "The details are shown above. If you're not sure what to do, ask your teacher."
291
+ )
91
292
 
92
293
  def init_direnv(self, modpath):
93
294
  if not (modpath / ".envrc").exists():
94
295
  (modpath / ".envrc").write_text("source .venv/bin/activate")
95
- run("direnv allow", shell=True, check=True, cwd=modpath)
296
+ try:
297
+ run("direnv allow", shell=True, check=True, cwd=modpath, capture_output=True)
298
+ except CalledProcessError:
299
+ raise GitError(
300
+ "Could not run `direnv allow` -- direnv doesn't seem to be installed "
301
+ "or isn't on your PATH. Try closing and reopening your terminal (so it "
302
+ "picks up changes from setup), then run `mwc update` again. If that "
303
+ "doesn't help, run `mwc setup` again or ask your teacher for help."
304
+ )
96
305
 
97
306
  def user_has_repo(self, repo_name, username=None):
98
- """Checks to see whether a user already has the named repo.
99
- """
307
+ """Checks to see whether a user already has the named repo."""
100
308
  username = username or self.settings['mwc_username']
101
309
  url = f"/repos/{username}/{repo_name}"
102
310
  response = self.authenticated_mwc_request('get', url)
@@ -115,7 +323,7 @@ class MWCBackend(GitBackend):
115
323
  def clone_repo(self, repo_name):
116
324
  user = self.settings['mwc_username']
117
325
  auth = user + ':' + self.settings['mwc_git_token']
118
- url = f"{self.MWC_GIT_PROTOCOL}://{auth}@{self.MWC_GIT_SERVER}/{user}/{repo_name}.git"
326
+ url = f"{self.MWC_GIT_PROTOCOL}://{auth}@{self.git_server}/{user}/{repo_name}.git"
119
327
  run(["git", "clone", url], check=True, capture_output=True)
120
328
 
121
329
  def check_settings(self):
@@ -135,7 +343,7 @@ class MWCBackend(GitBackend):
135
343
  return (server, repo_owner, repo_name)
136
344
 
137
345
  def authenticated_mwc_request(self, method_name, urlpath, data=None, params=None):
138
- server = self.MWC_GIT_PROTOCOL + '://' + self.MWC_GIT_SERVER
346
+ server = self.MWC_GIT_PROTOCOL + '://' + self.git_server
139
347
  args = {
140
348
  'url': server + "/api/v1" + urlpath,
141
349
  'auth': (self.settings['mwc_username'], self.settings['mwc_git_token']),
@@ -151,8 +359,7 @@ class MWCBackend(GitBackend):
151
359
  return response
152
360
 
153
361
  def relocate_existing_directory(self, path):
154
- """Moves an existing directory out of the way.
155
- """
362
+ """Moves an existing directory out of the way."""
156
363
  new_path = path.parent / path.name + '_old'
157
364
  while new_path.exists():
158
365
  new_path = new_path.parent / new_path.name + '_old'