Jujutsu/git integration
The editor now has a button to commit changes to a Jujutsu repository, which is automatically created if it's not present. A git remote is automatically set up using the URL template in settings.py. I use this with Forgejo's create-on-push feature to automatically create repositories on my Forgejo instance.
This commit is contained in:
parent
500eb38774
commit
d474a394f5
13 changed files with 913 additions and 376 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,3 +5,4 @@ public/
|
||||||
secret_key.txt
|
secret_key.txt
|
||||||
think_data/
|
think_data/
|
||||||
Makefile
|
Makefile
|
||||||
|
thinkserver/settings.py
|
||||||
|
|
|
@ -127,3 +127,10 @@ class RunCommandForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Think
|
model = Think
|
||||||
fields = []
|
fields = []
|
||||||
|
|
||||||
|
class GitCommitForm(forms.ModelForm):
|
||||||
|
message = forms.CharField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Think
|
||||||
|
fields = []
|
||||||
|
|
85
thinks/jujutsu.py
Normal file
85
thinks/jujutsu.py
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import functools
|
||||||
|
import subprocess
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
def ensure_jj(fn):
|
||||||
|
@functools.wraps(fn)
|
||||||
|
def ofn(self,*args,**kwargs):
|
||||||
|
self.init_jj()
|
||||||
|
return fn(self,*args,**kwargs)
|
||||||
|
|
||||||
|
return ofn
|
||||||
|
|
||||||
|
class JJController:
|
||||||
|
def __init__(self, think):
|
||||||
|
self.think = think
|
||||||
|
self.root = think.root
|
||||||
|
|
||||||
|
def run(self, cmd, **kwargs):
|
||||||
|
print("Run command",cmd)
|
||||||
|
res = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
cwd=self.root,
|
||||||
|
encoding='utf8',
|
||||||
|
capture_output=True,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def init_jj(self):
|
||||||
|
print("Init jj")
|
||||||
|
if not (self.root / '.jj').exists():
|
||||||
|
self.run(['jj','git','init'])
|
||||||
|
self.ignore_paths(['.make.*'])
|
||||||
|
git_url = settings.GIT_REPO_URL_TEMPLATE.format(name=self.think.slug)
|
||||||
|
self.run(['jj','git','remote','add','origin', git_url])
|
||||||
|
|
||||||
|
@ensure_jj
|
||||||
|
def status(self):
|
||||||
|
res = self.run(['jj','st'])
|
||||||
|
|
||||||
|
return res.stdout
|
||||||
|
|
||||||
|
def clean_paths(self, paths):
|
||||||
|
paths = [self.root / p for p in paths]
|
||||||
|
return [str(p.relative_to(self.root)) for p in paths if p.is_relative_to(self.root)]
|
||||||
|
|
||||||
|
@ensure_jj
|
||||||
|
def ignore_paths(self, paths):
|
||||||
|
paths = self.clean_paths(paths)
|
||||||
|
gitignore = self.root / '.gitignore'
|
||||||
|
if len(paths) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
if gitignore.exists():
|
||||||
|
with open(gitignore) as f:
|
||||||
|
ignored = f.read().strip().split('\n')
|
||||||
|
ignored += [p for p in paths if p not in ignored]
|
||||||
|
else:
|
||||||
|
ignored = paths
|
||||||
|
|
||||||
|
with open(gitignore, 'w') as f:
|
||||||
|
f.write('\n'.join(ignored))
|
||||||
|
|
||||||
|
@ensure_jj
|
||||||
|
def remove_paths(self, paths):
|
||||||
|
paths = self.clean_paths(paths)
|
||||||
|
if len(paths) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
return self.run(['git','rm'] + paths)
|
||||||
|
|
||||||
|
@ensure_jj
|
||||||
|
def add_paths(self, paths):
|
||||||
|
paths = self.clean_paths(paths)
|
||||||
|
if len(paths) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
return self.run(['git','add'] + paths)
|
||||||
|
|
||||||
|
@ensure_jj
|
||||||
|
def commit(self, message):
|
||||||
|
res = self.run(['jj','describe','--stdin','--no-edit'], input=message)
|
||||||
|
if res.returncode == 0:
|
||||||
|
self.run(['jj','new'])
|
||||||
|
return res
|
|
@ -3,6 +3,8 @@ from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.timezone import datetime, make_aware
|
from django.utils.timezone import datetime, make_aware
|
||||||
|
|
||||||
|
from .jujutsu import JJController
|
||||||
|
|
||||||
THINKS_DIR = settings.THINKS_DIR
|
THINKS_DIR = settings.THINKS_DIR
|
||||||
|
|
||||||
# Create your models here.
|
# Create your models here.
|
||||||
|
@ -21,6 +23,13 @@ class Think(models.Model):
|
||||||
def root(self):
|
def root(self):
|
||||||
return (THINKS_DIR / self.slug).resolve()
|
return (THINKS_DIR / self.slug).resolve()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def jj_controller(self):
|
||||||
|
return JJController(self)
|
||||||
|
|
||||||
|
def has_jj(self):
|
||||||
|
return (self.root / '.jj').exists()
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('think', kwargs={'slug': self.slug})
|
return reverse('think', kwargs={'slug': self.slug})
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ import './code-editor.mjs';
|
||||||
|
|
||||||
export default async function init_app() {
|
export default async function init_app() {
|
||||||
const flags = JSON.parse(document.getElementById('think-editor-data').textContent);
|
const flags = JSON.parse(document.getElementById('think-editor-data').textContent);
|
||||||
|
const packages = await (await fetch('https://elm-package-list.think.somethingorotherwhatever.com/elm-packages.json')).json();
|
||||||
|
flags.elm_packages = packages;
|
||||||
flags.csrf_token = document.getElementById('csrftoken')?.textContent || '';
|
flags.csrf_token = document.getElementById('csrftoken')?.textContent || '';
|
||||||
const app = Elm.App.init({node: document.body, flags});
|
const app = Elm.App.init({node: document.body, flags});
|
||||||
|
|
||||||
|
@ -16,4 +18,9 @@ export default async function init_app() {
|
||||||
},10);
|
},10);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
app.ports.show_modal.subscribe(id => {
|
||||||
|
console.log(id);
|
||||||
|
document.getElementById(id).showModal()
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,9 +1,24 @@
|
||||||
:root {
|
:root {
|
||||||
--spacing: 1em;
|
--spacing: 1em;
|
||||||
|
--quarter-spacing: calc(0.25 * var(--spacing));
|
||||||
--half-spacing: calc(0.5 * var(--spacing));
|
--half-spacing: calc(0.5 * var(--spacing));
|
||||||
--double-spacing: calc(2 * var(--spacing));
|
--double-spacing: calc(2 * var(--spacing));
|
||||||
|
|
||||||
|
--radius: 0.2em;
|
||||||
|
|
||||||
--editor-size: 50%;
|
--editor-size: 50%;
|
||||||
|
|
||||||
|
--background: hsl(70,100%,95%);
|
||||||
|
--color: black;
|
||||||
|
--button-bg: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
--background: hsl(70,100%,8%);
|
||||||
|
--color: white;
|
||||||
|
--button-bg: #333;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
@ -19,6 +34,9 @@ body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: var(--half-spacing);
|
padding: var(--half-spacing);
|
||||||
|
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--color);
|
||||||
|
|
||||||
& > header {
|
& > header {
|
||||||
padding: var(--spacing);
|
padding: var(--spacing);
|
||||||
|
@ -29,12 +47,6 @@ body {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
body {
|
|
||||||
background: black;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
header {
|
||||||
& #think-controls {
|
& #think-controls {
|
||||||
|
@ -53,6 +65,56 @@ header {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: thin solid currentColor;
|
||||||
|
--button-background: var(--button-bg);
|
||||||
|
--highlight: white;
|
||||||
|
--highlight-amount: 0%;
|
||||||
|
--darken: black;
|
||||||
|
--darken-amount: 0%;
|
||||||
|
background-color:
|
||||||
|
color-mix(in oklab,
|
||||||
|
color-mix(in oklab,
|
||||||
|
var(--button-background),
|
||||||
|
var(--darken) var(--darken-amount)
|
||||||
|
),
|
||||||
|
var(--highlight) var(--highlight-amount)
|
||||||
|
);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
--highlight-amount: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--highlight-amount: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
--highlight-amount: 0%;
|
||||||
|
--darken-amount: 10%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button[value="cancel"] {
|
||||||
|
--button-background: color-mix(in oklab, var(--button-bg), var(--background) 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:not([type="hidden"]) ~ button {
|
||||||
|
border-radius: 0 var(--radius) var(--radius) 0;
|
||||||
|
border-left: none;
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
border: thin solid currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--quarter-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
.think-editor {
|
.think-editor {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--spacing);
|
gap: var(--spacing);
|
||||||
|
@ -64,6 +126,12 @@ header {
|
||||||
"log editor preview" 1fr
|
"log editor preview" 1fr
|
||||||
/ var(--col-1-width) var(--editor-size) var(--preview-size)
|
/ var(--col-1-width) var(--editor-size) var(--preview-size)
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
border: thin solid currentColor;
|
||||||
|
padding: var(--quarter-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
&:has(#main-nav[open], #log[open]) {
|
&:has(#main-nav[open], #log[open]) {
|
||||||
--col-1-width: 20em;
|
--col-1-width: 20em;
|
||||||
|
@ -83,7 +151,7 @@ header {
|
||||||
}
|
}
|
||||||
|
|
||||||
& summary {
|
& summary {
|
||||||
background: #eee;
|
background: color-mix(in oklab, var(--background), var(--color) 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
& > #main-nav > nav {
|
& > #main-nav > nav {
|
||||||
|
@ -120,6 +188,17 @@ header {
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& #jj-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
gap: var(--spacing);
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
& #start-commit-button {
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
& #make-log {
|
& #make-log {
|
||||||
& > pre {
|
& > pre {
|
||||||
max-width: 20em;
|
max-width: 20em;
|
||||||
|
@ -129,16 +208,19 @@ header {
|
||||||
}
|
}
|
||||||
|
|
||||||
& #editor {
|
& #editor {
|
||||||
overflow: hidden;
|
overflow-x: hidden;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-basis: var(--editor-size);
|
flex-basis: var(--editor-size);
|
||||||
max-height: 85vh;
|
max-height: 85vh;
|
||||||
grid-area: editor;
|
grid-area: editor;
|
||||||
|
|
||||||
& #editor-controls {
|
& #editor-controls {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing);
|
gap: var(--spacing);
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
border-bottom: thin solid currentColor;
|
||||||
|
|
||||||
& > details {
|
& > details {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
@ -151,6 +233,8 @@ header {
|
||||||
margin: var(--half-spacing) 0;
|
margin: var(--half-spacing) 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
background: var(--background);
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
& #code-editor {
|
& #code-editor {
|
||||||
|
@ -189,13 +273,43 @@ header {
|
||||||
|
|
||||||
#file-form {
|
#file-form {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
max-height: 100%;
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dialog h2:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#jj-status {
|
||||||
|
max-height: 10em;
|
||||||
|
width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 6em;
|
||||||
|
resize-x: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog p:last-child {
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
gap: var(--spacing);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 100ch) {
|
@media (max-width: 100ch) {
|
||||||
html {
|
html {
|
||||||
font-size: min(3vw, 16px);
|
font-size: min(3vw, 16px);
|
||||||
}
|
}
|
||||||
|
body {
|
||||||
|
grid-template-columns: calc(100svw - var(--spacing));
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-size {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.think-editor {
|
.think-editor {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
grid-template:
|
grid-template:
|
||||||
|
@ -205,6 +319,8 @@ header {
|
||||||
"preview"
|
"preview"
|
||||||
;
|
;
|
||||||
|
|
||||||
|
padding-left: var(--double-spacing);
|
||||||
|
|
||||||
& > * ~ * {
|
& > * ~ * {
|
||||||
border-top: medium solid #888;
|
border-top: medium solid #888;
|
||||||
margin-top: var(--spacing);
|
margin-top: var(--spacing);
|
||||||
|
@ -219,12 +335,13 @@ header {
|
||||||
}
|
}
|
||||||
|
|
||||||
& #file-tree {
|
& #file-tree {
|
||||||
max-height: 7em;
|
max-height: 30svh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& #editor {
|
& #editor {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
max-height: revert;
|
||||||
& #code-editor {
|
& #code-editor {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -2,6 +2,14 @@
|
||||||
--spacing: 1em;
|
--spacing: 1em;
|
||||||
--half-spacing: calc(0.5 * var(--spacing));
|
--half-spacing: calc(0.5 * var(--spacing));
|
||||||
--double-spacing: calc(2 * var(--spacing));
|
--double-spacing: calc(2 * var(--spacing));
|
||||||
|
|
||||||
|
--background: hsl(70,100%,95%);
|
||||||
|
--color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
--background: hsl(70,100%,8%);
|
||||||
|
--color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
@ -9,6 +17,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
color-scheme: light dark;
|
||||||
|
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--color);
|
||||||
|
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
& > header {
|
& > header {
|
||||||
padding: var(--spacing);
|
padding: var(--spacing);
|
||||||
|
@ -26,22 +39,22 @@ body.login {
|
||||||
height: 100svh;
|
height: 100svh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: var(--spacing);
|
padding: var(--spacing);
|
||||||
|
|
||||||
& header {
|
& header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
& form {
|
& form {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--spacing);
|
gap: var(--spacing);
|
||||||
|
|
||||||
& div {
|
& div {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
gap: var(--spacing);
|
gap: var(--spacing);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
& label {
|
& label {
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
justify-self: end;
|
justify-self: end;
|
||||||
|
@ -51,7 +64,7 @@ body.login {
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
grid-template-rows: 2em 2em 2em;
|
grid-template-rows: 2em 2em 2em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,7 +89,7 @@ body.index {
|
||||||
gap: var(--double-spacing);
|
gap: var(--double-spacing);
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
& details {
|
& details {
|
||||||
&[open] > summary {
|
&[open] > summary {
|
||||||
margin-bottom: var(--spacing);
|
margin-bottom: var(--spacing);
|
||||||
|
@ -92,11 +105,25 @@ body.index {
|
||||||
& time {
|
& time {
|
||||||
font-size: smaller;
|
font-size: smaller;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .readme {
|
& .readme {
|
||||||
max-width: 80ch;
|
max-width: 80ch;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& .jj {
|
||||||
|
writing-mode: vertical-lr;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: 0.5em;
|
||||||
|
color: white;
|
||||||
|
background: black;
|
||||||
|
padding: 0.2em;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
|
||||||
|
&.has {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -240,3 +267,4 @@ body.thing-editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,15 +25,7 @@
|
||||||
|
|
||||||
<ul class="thinks-list">
|
<ul class="thinks-list">
|
||||||
{% for think in recent_thinks %}
|
{% for think in recent_thinks %}
|
||||||
<li class="think">
|
{% include "thinks/think_list_item.html" %}
|
||||||
<a href="{% url 'think' think.slug %}">{{think.slug}}</a>
|
|
||||||
<time datetime="{{think.creation_time|date:"c"}}">{{think.creation_time}}</time>
|
|
||||||
{% with readme=think.get_readme %}
|
|
||||||
{% if readme %}
|
|
||||||
<pre class="readme">{{readme|safe}}</pre>
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
@ -45,19 +37,11 @@
|
||||||
<ul class="thinks-list">
|
<ul class="thinks-list">
|
||||||
{% for category in categories %}
|
{% for category in categories %}
|
||||||
<li>
|
<li>
|
||||||
<details open>
|
<details>
|
||||||
<summary>{% if category.grouper %}{{category.grouper}}{% else %}Uncategorised{% endif %}</summary>
|
<summary>{% if category.grouper %}{{category.grouper}}{% else %}Uncategorised{% endif %}</summary>
|
||||||
<ul>
|
<ul>
|
||||||
{% for think in category.list %}
|
{% for think in category.list %}
|
||||||
<li class="think">
|
{% include "thinks/think_list_item.html" %}
|
||||||
<a href="{% url 'think' think.slug %}">{{think.slug}}</a>
|
|
||||||
<time datetime="{{think.creation_time|date:"c"}}">{{think.creation_time}}</time>
|
|
||||||
{% with readme=think.get_readme %}
|
|
||||||
{% if readme %}
|
|
||||||
<pre class="readme">{{readme|safe}}</pre>
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
|
|
10
thinks/templates/thinks/think_list_item.html
Normal file
10
thinks/templates/thinks/think_list_item.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<li class="think">
|
||||||
|
<a href="{% url 'think' think.slug %}">{{think.slug}}</a>
|
||||||
|
<time datetime="{{think.creation_time|date:"c"}}">{{think.creation_time}}</time>
|
||||||
|
<small class="jj {% if think.has_jj %}has{% endif %}">备份{% if think.has_jj %}✔{% else %}✗{% endif %}</small>
|
||||||
|
{% with readme=think.get_readme %}
|
||||||
|
{% if readme %}
|
||||||
|
<pre class="readme">{{readme|safe}}</pre>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</li>
|
|
@ -13,6 +13,8 @@ urlpatterns = [
|
||||||
path('think/<slug:slug>/delete-file', DeleteFileView.as_view(), name='delete_file'),
|
path('think/<slug:slug>/delete-file', DeleteFileView.as_view(), name='delete_file'),
|
||||||
path('think/<slug:slug>/run-command', RunCommandView.as_view(), name='run_command'),
|
path('think/<slug:slug>/run-command', RunCommandView.as_view(), name='run_command'),
|
||||||
path('think/<slug:slug>/log', LogView.as_view(), name='log'),
|
path('think/<slug:slug>/log', LogView.as_view(), name='log'),
|
||||||
|
path('think/<slug:slug>/jj/status', JJStatusView.as_view(), name='jj_status'),
|
||||||
|
path('think/<slug:slug>/jj/commit', JJCommitView.as_view(), name='jj_commit'),
|
||||||
path('new', CreateThinkView.as_view(), name='new_think'),
|
path('new', CreateThinkView.as_view(), name='new_think'),
|
||||||
path('new/<slug:slug>', RemixThinkView.as_view(), name='remix_think'),
|
path('new/<slug:slug>', RemixThinkView.as_view(), name='remix_think'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -223,3 +223,22 @@ class LogView(ThinkMixin, generic.DetailView):
|
||||||
think = self.get_object()
|
think = self.get_object()
|
||||||
|
|
||||||
return HttpResponse(think.get_log(), content_type='text/plain; charset=utf-8')
|
return HttpResponse(think.get_log(), content_type='text/plain; charset=utf-8')
|
||||||
|
|
||||||
|
class JJStatusView(ThinkMixin, generic.detail.DetailView):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
status = self.get_object().jj_controller.status()
|
||||||
|
return JsonResponse({'status': status})
|
||||||
|
|
||||||
|
class JJCommitView(ThinkMixin, generic.UpdateView):
|
||||||
|
form_class = forms.GitCommitForm
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
message = form.cleaned_data['message']
|
||||||
|
|
||||||
|
think = form.instance
|
||||||
|
|
||||||
|
jj = think.jj_controller
|
||||||
|
|
||||||
|
res = jj.commit(message)
|
||||||
|
|
||||||
|
return JsonResponse({'ok': res.returncode == 0, 'think': think.pk, 'stdout': res.stdout, 'stderr': res.stderr})
|
||||||
|
|
|
@ -131,3 +131,5 @@ THINKS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
THINKS_STATIC_URL = 'https://{slug}.think.somethingorotherwhatever.com'
|
THINKS_STATIC_URL = 'https://{slug}.think.somethingorotherwhatever.com'
|
||||||
|
|
||||||
API_TOKEN = ''
|
API_TOKEN = ''
|
||||||
|
|
||||||
|
GIT_REPO_URL_TEMPLATE = ''
|
||||||
|
|
Loading…
Reference in a new issue